From 277a20ec449011daab961e17b5c6bd7f48e3c291 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 9 May 2022 11:14:55 +0200 Subject: [PATCH 001/149] Fix `layout show` to not show changes when there are no changes (#297) fixes #295, partially Co-authored-by: Alex Auvolat Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/297 Co-authored-by: Alex Co-committed-by: Alex --- src/garage/cli/layout.rs | 25 +++++++++++++++++++++---- src/garage/cli/util.rs | 2 +- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/garage/cli/layout.rs b/src/garage/cli/layout.rs index e76f7737..88941d78 100644 --- a/src/garage/cli/layout.rs +++ b/src/garage/cli/layout.rs @@ -43,14 +43,22 @@ pub async fn cmd_assign_role( resp => return Err(Error::Message(format!("Invalid RPC response: {:?}", resp))), }; + let mut layout = fetch_layout(rpc_cli, rpc_host).await?; + let added_nodes = args .node_ids .iter() - .map(|node_id| find_matching_node(status.iter().map(|adv| adv.id), node_id)) + .map(|node_id| { + find_matching_node( + status + .iter() + .map(|adv| adv.id) + .chain(layout.node_ids().iter().cloned()), + node_id, + ) + }) .collect::, _>>()?; - let mut layout = fetch_layout(rpc_cli, rpc_host).await?; - let mut roles = layout.roles.clone(); roles.merge(&layout.staging); @@ -323,11 +331,20 @@ pub fn print_cluster_layout(layout: &ClusterLayout) -> bool { } pub fn print_staging_role_changes(layout: &ClusterLayout) -> bool { - if !layout.staging.items().is_empty() { + let has_changes = layout + .staging + .items() + .iter() + .any(|(k, _, v)| layout.roles.get(k) != Some(v)); + + if has_changes { println!(); println!("==== STAGED ROLE CHANGES ===="); let mut table = vec!["ID\tTags\tZone\tCapacity".to_string()]; for (id, _, role) in layout.staging.items().iter() { + if layout.roles.get(id) == Some(role) { + continue; + } if let Some(role) = &role.0 { let tags = role.tags.join(","); table.push(format!( diff --git a/src/garage/cli/util.rs b/src/garage/cli/util.rs index 7d496507..fe11ad44 100644 --- a/src/garage/cli/util.rs +++ b/src/garage/cli/util.rs @@ -208,7 +208,7 @@ pub fn find_matching_node( ) -> Result { let mut candidates = vec![]; for c in cand { - if hex::encode(&c).starts_with(&pattern) { + if hex::encode(&c).starts_with(&pattern) && !candidates.contains(&c) { candidates.push(c); } } From def78c5e6f5da37a0d17b5652c525fbeccbc2e86 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 9 May 2022 12:08:47 +0200 Subject: [PATCH 002/149] Update netapp to 0.4.4, fix #300 --- Cargo.lock | 12 ++++++------ Cargo.nix | 24 ++++++++++++------------ src/rpc/Cargo.toml | 2 +- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f61e2506..1469b37b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -840,7 +840,7 @@ dependencies = [ "http", "hyper", "kuska-sodiumoxide", - "netapp 0.4.2", + "netapp 0.4.4", "pretty_env_logger", "rand 0.8.5", "rmp-serde 0.15.5", @@ -975,7 +975,7 @@ dependencies = [ "garage_table 0.7.0", "garage_util 0.7.0", "hex", - "netapp 0.4.2", + "netapp 0.4.4", "opentelemetry", "rand 0.8.5", "rmp-serde 0.15.5", @@ -1031,7 +1031,7 @@ dependencies = [ "k8s-openapi", "kube", "kuska-sodiumoxide", - "netapp 0.4.2", + "netapp 0.4.4", "openssl", "opentelemetry", "pnet_datalink", @@ -1126,7 +1126,7 @@ dependencies = [ "hex", "http", "hyper", - "netapp 0.4.2", + "netapp 0.4.4", "opentelemetry", "rand 0.8.5", "rmp-serde 0.15.5", @@ -1833,9 +1833,9 @@ dependencies = [ [[package]] name = "netapp" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1a19af9ad24e6cdb166e2c5882a7ff9326cab8d45c9d82379fa638c0cbc84df" +checksum = "c6419a4b836774192e13fedb05c0e5f414ee8df9ca0c467456a0bacde06c29ee" dependencies = [ "arc-swap", "async-trait", diff --git a/Cargo.nix b/Cargo.nix index f7c9ec83..49f0a3d0 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -619,7 +619,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.config == "aarch64-linux-android" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -1193,7 +1193,7 @@ in git_version = rustPackages."registry+https://github.com/rust-lang/crates.io-index".git-version."0.3.5" { inherit profileName; }; hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; sodiumoxide = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; - netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.2" { inherit profileName; }; + netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; pretty_env_logger = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pretty_env_logger."0.4.0" { inherit profileName; }; rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; @@ -1348,7 +1348,7 @@ in garage_table = rustPackages."unknown".garage_table."0.7.0" { inherit profileName; }; garage_util = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; - netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.2" { inherit profileName; }; + netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; @@ -1415,7 +1415,7 @@ in ${ if rootFeatures' ? "garage_rpc" then "k8s_openapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".k8s-openapi."0.13.1" { inherit profileName; }; ${ if rootFeatures' ? "garage_rpc" then "kube" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kube."0.62.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "sodiumoxide" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.2" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; ${ if rootFeatures' ? "garage_rpc" then "openssl" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".openssl."0.10.38" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "pnet_datalink" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pnet_datalink."0.28.0" { inherit profileName; }; @@ -1518,7 +1518,7 @@ in hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; hyper = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; - netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.2" { inherit profileName; }; + netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; @@ -2456,11 +2456,11 @@ in }; }); - "registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.2" = overridableMkRustCrate (profileName: rec { + "registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" = overridableMkRustCrate (profileName: rec { name = "netapp"; - version = "0.4.2"; + version = "0.4.4"; registry = "registry+https://github.com/rust-lang/crates.io-index"; - src = fetchCratesIo { inherit name version; sha256 = "d1a19af9ad24e6cdb166e2c5882a7ff9326cab8d45c9d82379fa638c0cbc84df"; }; + src = fetchCratesIo { inherit name version; sha256 = "c6419a4b836774192e13fedb05c0e5f414ee8df9ca0c467456a0bacde06c29ee"; }; features = builtins.concatLists [ [ "default" ] (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "opentelemetry") @@ -3342,7 +3342,7 @@ in ]; dependencies = { ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" || hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "i686" || hostPlatform.parsed.cpu.name == "x86_64" || (hostPlatform.parsed.cpu.name == "aarch64" || hostPlatform.parsed.cpu.name == "armv6l" || hostPlatform.parsed.cpu.name == "armv7l") && (hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "fuchsia" || hostPlatform.parsed.kernel.name == "linux") then "spin" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".spin."0.5.2" { inherit profileName; }; untrusted = rustPackages."registry+https://github.com/rust-lang/crates.io-index".untrusted."0.7.1" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "wasm32" && hostPlatform.parsed.vendor.name == "unknown" && hostPlatform.parsed.kernel.name == "unknown" && hostPlatform.parsed.abi.name == "" then "web_sys" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.56" { inherit profileName; }; @@ -3819,7 +3819,7 @@ in ]; dependencies = { bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot_core."0.8.5" { inherit profileName; }; static_init_macro = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init_macro."1.0.2" { profileName = "__noProfile"; }; @@ -4789,11 +4789,11 @@ in [ "default" ] ]; dependencies = { - ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-uwp-windows-gnu" || hostPlatform.config == "i686-pc-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); diff --git a/src/rpc/Cargo.toml b/src/rpc/Cargo.toml index efaacf2e..46d0dc1e 100644 --- a/src/rpc/Cargo.toml +++ b/src/rpc/Cargo.toml @@ -48,7 +48,7 @@ opentelemetry = "0.17" #netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" } #netapp = { version = "0.4", path = "../../../netapp", features = ["telemetry"] } -netapp = { version = "0.4.2", features = ["telemetry"] } +netapp = { version = "0.4.4", features = ["telemetry"] } hyper = { version = "0.14", features = ["client", "http1", "runtime", "tcp"] } From 5768bf362262f78376af14517c4921941986192e Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 10 May 2022 13:16:57 +0200 Subject: [PATCH 003/149] First implementation of K2V (#293) **Specification:** View spec at [this URL](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/k2v/doc/drafts/k2v-spec.md) - [x] Specify the structure of K2V triples - [x] Specify the DVVS format used for causality detection - [x] Specify the K2V index (just a counter of number of values per partition key) - [x] Specify single-item endpoints: ReadItem, InsertItem, DeleteItem - [x] Specify index endpoint: ReadIndex - [x] Specify multi-item endpoints: InsertBatch, ReadBatch, DeleteBatch - [x] Move to JSON objects instead of tuples - [x] Specify endpoints for polling for updates on single values (PollItem) **Implementation:** - [x] Table for K2V items, causal contexts - [x] Indexing mechanism and table for K2V index - [x] Make API handlers a bit more generic - [x] K2V API endpoint - [x] K2V API router - [x] ReadItem - [x] InsertItem - [x] DeleteItem - [x] PollItem - [x] ReadIndex - [x] InsertBatch - [x] ReadBatch - [x] DeleteBatch **Testing:** - [x] Just a simple Python script that does some requests to check visually that things are going right (does not contain parsing of results or assertions on returned values) - [x] Actual tests: - [x] Adapt testing framework - [x] Simple test with InsertItem + ReadItem - [x] Test with several Insert/Read/DeleteItem + ReadIndex - [x] Test all combinations of return formats for ReadItem - [x] Test with ReadBatch, InsertBatch, DeleteBatch - [x] Test with PollItem - [x] Test error codes - [ ] Fix most broken stuff - [x] test PollItem broken randomly - [x] when invalid causality tokens are given, errors should be 4xx not 5xx **Improvements:** - [x] Descending range queries - [x] Specify - [x] Implement - [x] Add test - [x] Batch updates to index counter - [x] Put K2V behind `k2v` feature flag Co-authored-by: Alex Auvolat Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/293 Co-authored-by: Alex Co-committed-by: Alex --- Cargo.lock | 17 + Cargo.nix | 199 +++-- Makefile | 2 +- doc/drafts/k2v-spec.md | 680 +++++++++++++++++ k2v_test.py | 158 ++++ src/api/Cargo.toml | 5 + src/api/api_server.rs | 645 ---------------- src/api/error.rs | 7 +- src/api/generic_server.rs | 202 +++++ src/api/helpers.rs | 188 ++++- src/api/k2v/api_server.rs | 195 +++++ src/api/k2v/batch.rs | 368 +++++++++ src/api/k2v/index.rs | 100 +++ src/api/k2v/item.rs | 230 ++++++ src/api/k2v/mod.rs | 8 + src/api/k2v/range.rs | 96 +++ src/api/k2v/router.rs | 252 ++++++ src/api/lib.rs | 22 +- src/api/router_macros.rs | 190 +++++ src/api/s3/api_server.rs | 401 ++++++++++ src/api/{s3_bucket.rs => s3/bucket.rs} | 12 +- src/api/{s3_copy.rs => s3/copy.rs} | 14 +- src/api/{s3_cors.rs => s3/cors.rs} | 2 +- src/api/{s3_delete.rs => s3/delete.rs} | 4 +- src/api/{s3_get.rs => s3/get.rs} | 4 +- src/api/{s3_list.rs => s3/list.rs} | 92 +-- src/api/s3/mod.rs | 14 + .../{s3_post_object.rs => s3/post_object.rs} | 16 +- src/api/{s3_put.rs => s3/put.rs} | 8 +- src/api/{s3_router.rs => s3/router.rs} | 220 +----- src/api/{s3_website.rs => s3/website.rs} | 2 +- src/api/{s3_xml.rs => s3/xml.rs} | 0 src/api/signature/mod.rs | 9 +- src/api/signature/payload.rs | 15 +- src/api/signature/streaming.rs | 61 +- src/block/manager.rs | 2 +- src/garage/Cargo.toml | 8 + src/garage/admin.rs | 19 +- src/garage/cli/cmd.rs | 7 +- src/garage/repair.rs | 6 +- src/garage/server.rs | 26 +- src/garage/tests/common/client.rs | 2 +- src/garage/tests/common/custom_requester.rs | 55 +- src/garage/tests/common/garage.rs | 34 +- src/garage/tests/common/mod.rs | 11 +- src/garage/tests/k2v/batch.rs | 525 +++++++++++++ src/garage/tests/k2v/errorcodes.rs | 141 ++++ src/garage/tests/k2v/item.rs | 719 ++++++++++++++++++ src/garage/tests/k2v/mod.rs | 18 + src/garage/tests/k2v/poll.rs | 98 +++ src/garage/tests/k2v/simple.rs | 40 + src/garage/tests/lib.rs | 8 +- src/garage/tests/{ => s3}/list.rs | 0 src/garage/tests/s3/mod.rs | 6 + src/garage/tests/{ => s3}/multipart.rs | 0 src/garage/tests/{ => s3}/objects.rs | 0 src/garage/tests/{ => s3}/simple.rs | 0 .../tests/{ => s3}/streaming_signature.rs | 0 src/garage/tests/{ => s3}/website.rs | 32 +- src/model/Cargo.toml | 5 + src/model/garage.rs | 97 ++- src/model/helper/bucket.rs | 3 +- src/model/index_counter.rs | 305 ++++++++ src/model/k2v/causality.rs | 96 +++ src/model/k2v/counter_table.rs | 20 + src/model/k2v/item_table.rs | 291 +++++++ src/model/k2v/mod.rs | 7 + src/model/k2v/poll.rs | 50 ++ src/model/k2v/rpc.rs | 343 +++++++++ src/model/lib.rs | 9 +- src/model/{ => s3}/block_ref_table.rs | 8 +- src/model/s3/mod.rs | 3 + src/model/{ => s3}/object_table.rs | 7 +- src/model/{ => s3}/version_table.rs | 7 +- src/rpc/Cargo.toml | 1 + src/table/data.rs | 98 ++- src/table/schema.rs | 2 +- src/table/table.rs | 126 ++- src/table/util.rs | 18 +- src/util/Cargo.toml | 3 + src/util/config.rs | 16 +- src/util/error.rs | 3 + src/web/web_server.rs | 4 +- 83 files changed, 6491 insertions(+), 1226 deletions(-) create mode 100644 doc/drafts/k2v-spec.md create mode 100755 k2v_test.py delete mode 100644 src/api/api_server.rs create mode 100644 src/api/generic_server.rs create mode 100644 src/api/k2v/api_server.rs create mode 100644 src/api/k2v/batch.rs create mode 100644 src/api/k2v/index.rs create mode 100644 src/api/k2v/item.rs create mode 100644 src/api/k2v/mod.rs create mode 100644 src/api/k2v/range.rs create mode 100644 src/api/k2v/router.rs create mode 100644 src/api/router_macros.rs create mode 100644 src/api/s3/api_server.rs rename src/api/{s3_bucket.rs => s3/bucket.rs} (97%) rename src/api/{s3_copy.rs => s3/copy.rs} (98%) rename src/api/{s3_cors.rs => s3/cors.rs} (99%) rename src/api/{s3_delete.rs => s3/delete.rs} (98%) rename src/api/{s3_get.rs => s3/get.rs} (99%) rename src/api/{s3_list.rs => s3/list.rs} (94%) create mode 100644 src/api/s3/mod.rs rename src/api/{s3_post_object.rs => s3/post_object.rs} (98%) rename src/api/{s3_put.rs => s3/put.rs} (99%) rename src/api/{s3_router.rs => s3/router.rs} (81%) rename src/api/{s3_website.rs => s3/website.rs} (99%) rename src/api/{s3_xml.rs => s3/xml.rs} (100%) create mode 100644 src/garage/tests/k2v/batch.rs create mode 100644 src/garage/tests/k2v/errorcodes.rs create mode 100644 src/garage/tests/k2v/item.rs create mode 100644 src/garage/tests/k2v/mod.rs create mode 100644 src/garage/tests/k2v/poll.rs create mode 100644 src/garage/tests/k2v/simple.rs rename src/garage/tests/{ => s3}/list.rs (100%) create mode 100644 src/garage/tests/s3/mod.rs rename src/garage/tests/{ => s3}/multipart.rs (100%) rename src/garage/tests/{ => s3}/objects.rs (100%) rename src/garage/tests/{ => s3}/simple.rs (100%) rename src/garage/tests/{ => s3}/streaming_signature.rs (100%) rename src/garage/tests/{ => s3}/website.rs (92%) create mode 100644 src/model/index_counter.rs create mode 100644 src/model/k2v/causality.rs create mode 100644 src/model/k2v/counter_table.rs create mode 100644 src/model/k2v/item_table.rs create mode 100644 src/model/k2v/mod.rs create mode 100644 src/model/k2v/poll.rs create mode 100644 src/model/k2v/rpc.rs rename src/model/{ => s3}/block_ref_table.rs (85%) create mode 100644 src/model/s3/mod.rs rename src/model/{ => s3}/object_table.rs (98%) rename src/model/{ => s3}/version_table.rs (96%) diff --git a/Cargo.lock b/Cargo.lock index 1469b37b..de1ae5cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,16 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "assert-json-diff" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f1c3703dd33532d7f0ca049168930e9099ecac238e23cf932f3a69c42f06da" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-stream" version = "0.3.3" @@ -821,8 +831,10 @@ dependencies = [ name = "garage" version = "0.7.0" dependencies = [ + "assert-json-diff", "async-trait", "aws-sdk-s3", + "base64", "bytes 1.1.0", "chrono", "futures", @@ -846,6 +858,7 @@ dependencies = [ "rmp-serde 0.15.5", "serde", "serde_bytes", + "serde_json", "sha2", "sled", "static_init", @@ -876,6 +889,7 @@ dependencies = [ name = "garage_api" version = "0.7.0" dependencies = [ + "async-trait", "base64", "bytes 1.1.0", "chrono", @@ -886,6 +900,7 @@ dependencies = [ "futures-util", "garage_block", "garage_model 0.7.0", + "garage_rpc 0.7.0", "garage_table 0.7.0", "garage_util 0.7.0", "hex", @@ -966,6 +981,8 @@ version = "0.7.0" dependencies = [ "arc-swap", "async-trait", + "base64", + "blake2", "err-derive 0.3.1", "futures", "futures-util", diff --git a/Cargo.nix b/Cargo.nix index 49f0a3d0..39f409b6 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -98,6 +98,17 @@ in ]; }); + "registry+https://github.com/rust-lang/crates.io-index".assert-json-diff."2.0.1" = overridableMkRustCrate (profileName: rec { + name = "assert-json-diff"; + version = "2.0.1"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "50f1c3703dd33532d7f0ca049168930e9099ecac238e23cf932f3a69c42f06da"; }; + dependencies = { + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".async-stream."0.3.3" = overridableMkRustCrate (profileName: rec { name = "async-stream"; version = "0.3.3"; @@ -554,7 +565,7 @@ in [ "default" ] [ "libc" ] [ "oldtime" ] - (lib.optional (rootFeatures' ? "garage_rpc") "serde") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "serde") [ "std" ] [ "time" ] [ "winapi" ] @@ -563,7 +574,7 @@ in libc = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; num_integer = rustPackages."registry+https://github.com/rust-lang/crates.io-index".num-integer."0.1.44" { inherit profileName; }; num_traits = rustPackages."registry+https://github.com/rust-lang/crates.io-index".num-traits."0.2.14" { inherit profileName; }; - ${ if rootFeatures' ? "garage_rpc" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; time = rustPackages."registry+https://github.com/rust-lang/crates.io-index".time."0.1.44" { inherit profileName; }; ${ if hostPlatform.isWindows then "winapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".winapi."0.3.9" { inherit profileName; }; }; @@ -619,7 +630,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -1178,6 +1189,10 @@ in version = "0.7.0"; registry = "unknown"; src = fetchCrateLocal (workspaceSrc + "/src/garage"); + features = builtins.concatLists [ + [ "k2v" ] + [ "kubernetes-discovery" ] + ]; dependencies = { async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; @@ -1206,11 +1221,14 @@ in tracing = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; }; devDependencies = { + assert_json_diff = rustPackages."registry+https://github.com/rust-lang/crates.io-index".assert-json-diff."2.0.1" { inherit profileName; }; aws_sdk_s3 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-sdk-s3."0.8.0" { inherit profileName; }; + base64 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.13.0" { inherit profileName; }; chrono = rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.19" { inherit profileName; }; hmac = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hmac."0.10.1" { inherit profileName; }; http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; hyper = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; + serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" { inherit profileName; }; sha2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sha2."0.9.9" { inherit profileName; }; static_init = rustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init."1.0.2" { inherit profileName; }; }; @@ -1241,41 +1259,46 @@ in version = "0.7.0"; registry = "unknown"; src = fetchCrateLocal (workspaceSrc + "/src/api"); + features = builtins.concatLists [ + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "k2v") + ]; dependencies = { - base64 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.13.0" { inherit profileName; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; - chrono = rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.19" { inherit profileName; }; - crypto_mac = rustPackages."registry+https://github.com/rust-lang/crates.io-index".crypto-mac."0.10.1" { inherit profileName; }; - err_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; - form_urlencoded = rustPackages."registry+https://github.com/rust-lang/crates.io-index".form_urlencoded."1.0.1" { inherit profileName; }; - futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; - futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; - garage_block = rustPackages."unknown".garage_block."0.7.0" { inherit profileName; }; - garage_model = rustPackages."unknown".garage_model."0.7.0" { inherit profileName; }; - garage_table = rustPackages."unknown".garage_table."0.7.0" { inherit profileName; }; - garage_util = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; - hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; - hmac = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hmac."0.10.1" { inherit profileName; }; - http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; - http_range = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-range."0.1.5" { inherit profileName; }; - httpdate = rustPackages."registry+https://github.com/rust-lang/crates.io-index".httpdate."0.3.2" { inherit profileName; }; - hyper = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; - idna = rustPackages."registry+https://github.com/rust-lang/crates.io-index".idna."0.2.3" { inherit profileName; }; - md5 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".md-5."0.9.1" { inherit profileName; }; - multer = rustPackages."registry+https://github.com/rust-lang/crates.io-index".multer."2.0.2" { inherit profileName; }; - nom = rustPackages."registry+https://github.com/rust-lang/crates.io-index".nom."7.1.1" { inherit profileName; }; - opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; - percent_encoding = rustPackages."registry+https://github.com/rust-lang/crates.io-index".percent-encoding."2.1.0" { inherit profileName; }; - pin_project = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; }; - quick_xml = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quick-xml."0.21.0" { inherit profileName; }; - roxmltree = rustPackages."registry+https://github.com/rust-lang/crates.io-index".roxmltree."0.14.1" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; - serde_bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; - serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" { inherit profileName; }; - sha2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sha2."0.9.9" { inherit profileName; }; - tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; - tracing = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; - url = rustPackages."registry+https://github.com/rust-lang/crates.io-index".url."2.2.2" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "async_trait" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "base64" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.13.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "chrono" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.19" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "crypto_mac" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".crypto-mac."0.10.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "err_derive" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "form_urlencoded" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".form_urlencoded."1.0.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "garage_block" else null } = rustPackages."unknown".garage_block."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "garage_model" else null } = rustPackages."unknown".garage_model."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "garage_rpc" else null } = rustPackages."unknown".garage_rpc."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "garage_table" else null } = rustPackages."unknown".garage_table."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "hmac" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hmac."0.10.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "http" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "http_range" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-range."0.1.5" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "httpdate" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".httpdate."0.3.2" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "hyper" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "idna" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".idna."0.2.3" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "md5" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".md-5."0.9.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "multer" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".multer."2.0.2" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "nom" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".nom."7.1.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "percent_encoding" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".percent-encoding."2.1.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "pin_project" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "quick_xml" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quick-xml."0.21.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "roxmltree" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".roxmltree."0.14.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "serde_bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "serde_json" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "sha2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sha2."0.9.9" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "tracing" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "url" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".url."2.2.2" { inherit profileName; }; }; }); @@ -1336,28 +1359,33 @@ in version = "0.7.0"; registry = "unknown"; src = fetchCrateLocal (workspaceSrc + "/src/model"); + features = builtins.concatLists [ + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model") "k2v") + ]; dependencies = { - arc_swap = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; - async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; - err_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; - futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; - futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; - garage_block = rustPackages."unknown".garage_block."0.7.0" { inherit profileName; }; - garage_model_050 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".garage_model."0.5.1" { inherit profileName; }; - garage_rpc = rustPackages."unknown".garage_rpc."0.7.0" { inherit profileName; }; - garage_table = rustPackages."unknown".garage_table."0.7.0" { inherit profileName; }; - garage_util = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; - hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; - netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; - opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; - rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; - rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; - serde_bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; - sled = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; - tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; - tracing = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; - zstd = rustPackages."registry+https://github.com/rust-lang/crates.io-index".zstd."0.9.2+zstd.1.5.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "arc_swap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "async_trait" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "base64" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.13.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "blake2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".blake2."0.9.2" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "err_derive" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_block" else null } = rustPackages."unknown".garage_block."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_model_050" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".garage_model."0.5.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_rpc" else null } = rustPackages."unknown".garage_rpc."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_table" else null } = rustPackages."unknown".garage_table."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "serde_bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "sled" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "tracing" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "zstd" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".zstd."0.9.2+zstd.1.5.1" { inherit profileName; }; }; }); @@ -1395,11 +1423,11 @@ in registry = "unknown"; src = fetchCrateLocal (workspaceSrc + "/src/rpc"); features = builtins.concatLists [ - (lib.optional (rootFeatures' ? "garage_rpc") "k8s-openapi") - (lib.optional (rootFeatures' ? "garage_rpc") "kube") - (lib.optional (rootFeatures' ? "garage_rpc") "kubernetes-discovery") - (lib.optional (rootFeatures' ? "garage_rpc") "openssl") - (lib.optional (rootFeatures' ? "garage_rpc") "schemars") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "k8s-openapi") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "kube") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "kubernetes-discovery") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "openssl") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "schemars") ]; dependencies = { ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "arc_swap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; @@ -1412,16 +1440,16 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "gethostname" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".gethostname."0.2.3" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "hyper" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; - ${ if rootFeatures' ? "garage_rpc" then "k8s_openapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".k8s-openapi."0.13.1" { inherit profileName; }; - ${ if rootFeatures' ? "garage_rpc" then "kube" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kube."0.62.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "k8s_openapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".k8s-openapi."0.13.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "kube" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kube."0.62.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "sodiumoxide" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; - ${ if rootFeatures' ? "garage_rpc" then "openssl" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".openssl."0.10.38" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "openssl" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".openssl."0.10.38" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "pnet_datalink" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pnet_datalink."0.28.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; - ${ if rootFeatures' ? "garage_rpc" then "schemars" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".schemars."0.8.8" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "schemars" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".schemars."0.8.8" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "serde_bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "serde_json" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" { inherit profileName; }; @@ -1510,6 +1538,9 @@ in version = "0.7.0"; registry = "unknown"; src = fetchCrateLocal (workspaceSrc + "/src/util"); + features = builtins.concatLists [ + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_util") "k2v") + ]; dependencies = { blake2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".blake2."0.9.2" { inherit profileName; }; chrono = rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.19" { inherit profileName; }; @@ -2361,7 +2392,7 @@ in [ "os-poll" ] ]; dependencies = { - ${ if hostPlatform.parsed.kernel.name == "wasi" || hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "wasi" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if hostPlatform.isWindows then "miow" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".miow."0.3.7" { inherit profileName; }; ${ if hostPlatform.isWindows then "ntapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ntapi."0.3.7" { inherit profileName; }; @@ -3342,7 +3373,7 @@ in ]; dependencies = { ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" || hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "i686" || hostPlatform.parsed.cpu.name == "x86_64" || (hostPlatform.parsed.cpu.name == "aarch64" || hostPlatform.parsed.cpu.name == "armv6l" || hostPlatform.parsed.cpu.name == "armv7l") && (hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "fuchsia" || hostPlatform.parsed.kernel.name == "linux") then "spin" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".spin."0.5.2" { inherit profileName; }; untrusted = rustPackages."registry+https://github.com/rust-lang/crates.io-index".untrusted."0.7.1" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "wasm32" && hostPlatform.parsed.vendor.name == "unknown" && hostPlatform.parsed.kernel.name == "unknown" && hostPlatform.parsed.abi.name == "" then "web_sys" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.56" { inherit profileName; }; @@ -3556,12 +3587,12 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"; }; features = builtins.concatLists [ - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "OSX_10_9") - (lib.optional (rootFeatures' ? "garage_rpc") "default") + [ "OSX_10_9" ] + [ "default" ] ]; dependencies = { - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "core_foundation_sys" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".core-foundation-sys."0.8.3" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + core_foundation_sys = rustPackages."registry+https://github.com/rust-lang/crates.io-index".core-foundation-sys."0.8.3" { inherit profileName; }; + libc = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -3652,12 +3683,12 @@ in src = fetchCratesIo { inherit name version; sha256 = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"; }; features = builtins.concatLists [ [ "default" ] - (lib.optional (rootFeatures' ? "garage_rpc") "indexmap") - (lib.optional (rootFeatures' ? "garage_rpc") "preserve_order") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "indexmap") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "preserve_order") [ "std" ] ]; dependencies = { - ${ if rootFeatures' ? "garage_rpc" then "indexmap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".indexmap."1.8.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "indexmap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".indexmap."1.8.0" { inherit profileName; }; itoa = rustPackages."registry+https://github.com/rust-lang/crates.io-index".itoa."1.0.1" { inherit profileName; }; ryu = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ryu."1.0.9" { inherit profileName; }; serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; @@ -4157,8 +4188,8 @@ in (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "default") [ "futures-io" ] (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "io") - (lib.optional (rootFeatures' ? "garage_rpc") "slab") - (lib.optional (rootFeatures' ? "garage_rpc") "time") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "slab") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "time") ]; dependencies = { bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; @@ -4167,7 +4198,7 @@ in futures_sink = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-sink."0.3.21" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; - ${ if rootFeatures' ? "garage_rpc" then "slab" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".slab."0.4.5" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "slab" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".slab."0.4.5" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; }; }); @@ -4708,7 +4739,7 @@ in [ "in6addr" ] [ "inaddr" ] [ "ioapiset" ] - (lib.optional (rootFeatures' ? "garage_rpc") "knownfolders") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "knownfolders") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "lmcons") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "minschannel") [ "minwinbase" ] @@ -4718,13 +4749,13 @@ in [ "ntdef" ] [ "ntsecapi" ] [ "ntstatus" ] - (lib.optional (rootFeatures' ? "garage_rpc") "objbase") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "objbase") [ "processenv" ] [ "processthreadsapi" ] [ "profileapi" ] (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "schannel") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "securitybaseapi") - (lib.optional (rootFeatures' ? "garage_rpc") "shlobj") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "shlobj") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "sspi") [ "std" ] [ "synchapi" ] @@ -4792,8 +4823,8 @@ in ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-uwp-windows-gnu" || hostPlatform.config == "i686-pc-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); diff --git a/Makefile b/Makefile index c0ebc075..c70be9da 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: doc all release shell all: - clear; cargo build + clear; cargo build --features k2v doc: cd doc/book; mdbook build diff --git a/doc/drafts/k2v-spec.md b/doc/drafts/k2v-spec.md new file mode 100644 index 00000000..08809069 --- /dev/null +++ b/doc/drafts/k2v-spec.md @@ -0,0 +1,680 @@ +# Specification of the Garage K2V API (K2V = Key/Key/Value) + +- We are storing triplets of the form `(partition key, sort key, value)` -> no + user-defined fields, the client is responsible of writing whatever he wants + in the value (typically an encrypted blob). Values are binary blobs, which + are always represented as their base64 encoding in the JSON API. Partition + keys and sort keys are utf8 strings. + +- Triplets are stored in buckets; each bucket stores a separate set of triplets + +- Bucket names and access keys are the same as for accessing the S3 API + +- K2V triplets exist separately from S3 objects. K2V triplets don't exist for + the S3 API, and S3 objects don't exist for the K2V API. + +- Values stored for triplets have associated causality information, that enables + Garage to detect concurrent writes. In case of concurrent writes, Garage + keeps the concurrent values until a further write supersedes the concurrent + values. This is the same method as Riak KV implements. The method used is + based on DVVS (dotted version vector sets), described in the paper "Scalable + and Accurate Causality Tracking for Eventually Consistent Data Stores", as + well as [here](https://github.com/ricardobcl/Dotted-Version-Vectors) + + +## Data format + +### Triple format + +Triples in K2V are constituted of three fields: + +- a partition key (`pk`), an utf8 string that defines in what partition the + triplet is stored; triplets in different partitions cannot be listed together + in a ReadBatch command, or deleted together in a DeleteBatch command: a + separate command must be included in the ReadBatch/DeleteBatch call for each + partition key in which the client wants to read/delete lists of items + +- a sort key (`sk`), an utf8 string that defines the index of the triplet inside its + partition; triplets are uniquely idendified by their partition key + sort key + +- a value (`v`), an opaque binary blob associated to the partition key + sort key; + they are transmitted as binary when possible but in most case in the JSON API + they will be represented as strings using base64 encoding; a value can also + be `null` to indicate a deleted triplet (a `null` value is called a tombstone) + +### Causality information + +K2V supports storing several concurrent values associated to a pk+sk, in the +case where insertion or deletion operations are detected to be concurrent (i.e. +there is not one that was aware of the other, they are not causally dependant +one on the other). In practice, it even looks more like the opposite: to +overwrite a previously existing value, the client must give a "causality token" +that "proves" (not in a cryptographic sense) that it had seen a previous value. +Otherwise, the value written will not overwrite an existing value, it will just +create a new concurrent value. + +The causality token is a binary/b64-encoded representation of a context, +specified below. + +A set of concurrent values looks like this: + +``` +(node1, tdiscard1, (v1, t1), (v2, t2)) ; tdiscard1 < t1 < t2 +(node2, tdiscard2, (v3, t3) ; tdiscard2 < t3 +``` + +`tdiscard` for a node `i` means that all values inserted by node `i` with times +`<= tdiscard` are obsoleted, i.e. have been read by a client that overwrote it +afterwards. + +The associated context would be the following: `[(node1, t2), (node2, t3)]`, +i.e. if a node reads this set of values and inserts a new values, we will now +have `tdiscard1 = t2` and `tdiscard2 = t3`, to indicate that values v1, v2 and v3 +are obsoleted by the new write. + +**Basic insertion.** To insert a new value `v4` with context `[(node1, t2), (node2, t3)]`, in a +simple case where there was no insertion in-between reading the value +mentionned above and writing `v4`, and supposing that node2 receives the +InsertItem query: + +- `node2` generates a timestamp `t4` such that `t4 > t3`. +- the new state is as follows: + +``` +(node1, tdiscard1', ()) ; tdiscard1' = t2 +(node2, tdiscard2', (v4, t4)) ; tdiscard2' = t3 +``` + +**A more complex insertion example.** In the general case, other intermediate values could have +been written before `v4` with context `[(node1, t2), (node2, t3)]` is sent to the system. +For instance, here is a possible sequence of events: + +1. First we have the set of values v1, v2 and v3 described above. + A node reads it, it obtains values v1, v2 and v3 with context `[(node1, t2), (node2, t3)]`. + +2. A node writes a value `v5` with context `[(node1, t1)]`, i.e. `v5` is only a + successor of v1 but not of v2 or v3. Suppose node1 receives the write, it + will generate a new timestamp `t5` larger than all of the timestamps it + knows of, i.e. `t5 > t2`. We will now have: + +``` +(node1, tdiscard1'', (v2, t2), (v5, t5)) ; tdiscard1'' = t1 < t2 < t5 +(node2, tdiscard2, (v3, t3) ; tdiscard2 < t3 +``` + +3. Now `v4` is written with context `[(node1, t2), (node2, t3)]`, and node2 + processes the query. It will generate `t4 > t3` and the state will become: + +``` +(node1, tdiscard1', (v5, t5)) ; tdiscard1' = t2 < t5 +(node2, tdiscard2', (v4, t4)) ; tdiscard2' = t3 +``` + +**Generic algorithm for handling insertions:** A certain node n handles the +InsertItem and is responsible for the correctness of this procedure. + +1. Lock the key (or the whole table?) at this node to prevent concurrent updates of the value that would mess things up +2. Read current set of values +3. Generate a new timestamp that is larger than the largest timestamp for node n +4. Add the inserted value in the list of values of node n +5. Update the discard times to be the times set in the context, and accordingly discard overwritten values +6. Release lock +7. Propagate updated value to other nodes +8. Return to user when propagation achieved the write quorum (propagation to other nodes continues asynchronously) + +**Encoding of contexts:** + +Contexts consist in a list of (node id, timestamp) pairs. +They are encoded in binary as follows: + +``` +checksum: u64, [ node: u64, timestamp: u64 ]* +``` + +The checksum is just the XOR of all of the node IDs and timestamps. + +Once encoded in binary, contexts are written and transmitted in base64. + + +### Indexing + +K2V keeps an index, a secondary data structure that is updated asynchronously, +that keeps tracks of the number of triplets stored for each partition key. +This allows easy listing of all of the partition keys for which triplets exist +in a bucket, as the partition key becomes the sort key in the index. + +How indexing works: + +- Each node keeps a local count of how many items it stores for each partition, + in a local Sled tree that is updated atomically when an item is modified. +- These local counters are asynchronously stored in the index table which is + a regular Garage table spread in the network. Counters are stored as LWW values, + so basically the final table will have the following structure: + +``` +- pk: bucket +- sk: partition key for which we are counting +- v: lwwmap (node id -> number of items) +``` + +The final number of items present in the partition can be estimated by taking +the maximum of the values (i.e. the value for the node that announces having +the most items for that partition). In most cases the values for different node +IDs should all be the same; more precisely, three node IDs should map to the +same non-zero value, and all other node IDs that are present are tombstones +that map to zeroes. Note that we need to filter out values from nodes that are +no longer part of the cluster layout, as when nodes are removed they won't +necessarily have had the time to set their counters to zero. + +## Important details + +**THIS SECTION CONTAINS A FEW WARNINGS ON THE K2V API WHICH ARE IMPORTANT +TO UNDERSTAND IN ORDER TO USE IT CORRECTLY.** + +- **Internal server errors on updates do not mean that the update isn't stored.** + K2V will return an internal server error when it cannot reach a quorum of nodes on + which to save an updated value. However the value may still be stored on just one + node, which will then propagate it to other nodes asynchronously via anti-entropy. + +- **Batch operations are not transactions.** When calling InsertBatch or DeleteBatch, + items may appear partially inserted/deleted while the operation is being processed. + More importantly, if InsertBatch or DeleteBatch returns an internal server error, + some of the items to be inserted/deleted might end up inserted/deleted on the server, + while others may still have their old value. + +- **Concurrent values are deduplicated.** When inserting a value for a key, + Garage might internally end up + storing the value several times if there are network errors. These values will end up as + concurrent values for a key, with the same byte string (or `null` for a deletion). + Garage fixes this by deduplicating concurrent values when they are returned to the + user on read operations. Importantly, *Garage does not differentiate between duplicate + concurrent values due to the user making the same call twice, or Garage having to + do an internal retry*. This means that all duplicate concurrent values are deduplicated + when an item is read: if the user inserts twice concurrently the same value, they will + only read it once. + +## API Endpoints + +### Operations on single items + +**ReadItem: `GET //?sort_key=`** + + +Query parameters: + +| name | default value | meaning | +| - | - | - | +| `sort_key` | **mandatory** | The sort key of the item to read | + +Returns the item with specified partition key and sort key. Values can be +returned in either of two ways: + +1. a JSON array of base64-encoded values, or `null`'s for tombstones, with + header `Content-Type: application/json` + +2. in the case where there are no concurrent values, the single present value + can be returned directly as the response body (or an HTTP 204 NO CONTENT for + a tombstone), with header `Content-Type: application/octet-stream` + +The choice between return formats 1 and 2 is directed by the `Accept` HTTP header: + +- if the `Accept` header is not present, format 1 is always used + +- if `Accept` contains `application/json` but not `application/octet-stream`, + format 1 is always used + +- if `Accept` contains `application/octet-stream` but not `application/json`, + format 2 is used when there is a single value, and an HTTP error 409 (HTTP + 409 CONFLICT) is returned in the case of multiple concurrent values + (including concurrent tombstones) + +- if `Accept` contains both, format 2 is used when there is a single value, and + format 1 is used as a fallback in case of concurrent values + +- if `Accept` contains none, HTTP 406 NOT ACCEPTABLE is raised + +Example query: + +``` +GET /my_bucket/mailboxes?sort_key=INBOX HTTP/1.1 +``` + +Example response: + +```json +HTTP/1.1 200 OK +X-Garage-Causality-Token: opaquetoken123 +Content-Type: application/json + +[ + "b64cryptoblob123", + "b64cryptoblob'123" +] +``` + +Example response in case the item is a tombstone: + +``` +HTTP/1.1 200 OK +X-Garage-Causality-Token: opaquetoken999 +Content-Type: application/json + +[ + null +] +``` + +Example query 2: + +``` +GET /my_bucket/mailboxes?sort_key=INBOX HTTP/1.1 +Accept: application/octet-stream +``` + +Example response if multiple concurrent versions exist: + +``` +HTTP/1.1 409 CONFLICT +X-Garage-Causality-Token: opaquetoken123 +Content-Type: application/octet-stream +``` + +Example response in case of single value: + +``` +HTTP/1.1 200 OK +X-Garage-Causality-Token: opaquetoken123 +Content-Type: application/octet-stream + +cryptoblob123 +``` + +Example response in case of a single value that is a tombstone: + +``` +HTTP/1.1 204 NO CONTENT +X-Garage-Causality-Token: opaquetoken123 +Content-Type: application/octet-stream +``` + + +**PollItem: `GET //?sort_key=&causality_token=`** + +This endpoint will block until a new value is written to a key. + +The GET parameter `causality_token` should be set to the causality +token returned with the last read of the key, so that K2V knows +what values are concurrent or newer than the ones that the +client previously knew. + +This endpoint returns the new value in the same format as ReadItem. +If no new value is written and the timeout elapses, +an HTTP 304 NOT MODIFIED is returned. + +Query parameters: + +| name | default value | meaning | +| - | - | - | +| `sort_key` | **mandatory** | The sort key of the item to read | +| `causality_token` | **mandatory** | The causality token of the last known value or set of values | +| `timeout` | 300 | The timeout before 304 NOT MODIFIED is returned if the value isn't updated | + +The timeout can be set to any number of seconds, with a maximum of 600 seconds (10 minutes). + + +**InsertItem: `PUT //?sort_key=`** + +Inserts a single item. This request does not use JSON, the body is sent directly as a binary blob. + +To supersede previous values, the HTTP header `X-Garage-Causality-Token` should +be set to the causality token returned by a previous read on this key. This +header can be ommitted for the first writes to the key. + +Example query: + +``` +PUT /my_bucket/mailboxes?sort_key=INBOX HTTP/1.1 +X-Garage-Causality-Token: opaquetoken123 + +myblobblahblahblah +``` + +Example response: + +``` +HTTP/1.1 200 OK +``` + +**DeleteItem: `DELETE //?sort_key=`** + +Deletes a single item. The HTTP header `X-Garage-Causality-Token` must be set +to the causality token returned by a previous read on this key, to indicate +which versions of the value should be deleted. The request will not process if +`X-Garage-Causality-Token` is not set. + +Example query: + +``` +DELETE /my_bucket/mailboxes?sort_key=INBOX HTTP/1.1 +X-Garage-Causality-Token: opaquetoken123 +``` + +Example response: + +``` +HTTP/1.1 204 NO CONTENT +``` + +### Operations on index + +**ReadIndex: `GET /?start=&end=&limit=`** + +Lists all partition keys in the bucket for which some triplets exist, and gives +for each the number of triplets (or an approximation thereof, this value is + asynchronously updated, and thus eventually consistent). + +Query parameters: + +| name | default value | meaning | +| - | - | - | +| `prefix` | `null` | Restrict listing to partition keys that start with this prefix | +| `start` | `null` | First partition key to list, in lexicographical order | +| `end` | `null` | Last partition key to list (excluded) | +| `limit` | `null` | Maximum number of partition keys to list | +| `reverse` | `false` | Iterate in reverse lexicographical order | + +The response consists in a JSON object that repeats the parameters of the query and gives the result (see below). + +The listing starts at partition key `start`, or if not specified at the +smallest partition key that exists. It returns partition keys in increasing +order, or decreasing order if `reverse` is set to `true`, +and stops when either of the following conditions is met: + +1. if `end` is specfied, the partition key `end` is reached or surpassed (if it + is reached exactly, it is not included in the result) + +2. if `limit` is specified, `limit` partition keys have been listed + +3. no more partition keys are available to list + +In case 2, and if there are more partition keys to list before condition 1 +triggers, then in the result `more` is set to `true` and `nextStart` is set to +the first partition key that couldn't be listed due to the limit. In the first +case (if the listing stopped because of the `end` parameter), `more` is not set +and the `nextStart` key is not specified. + +Note that if `reverse` is set to `true`, `start` is the highest key +(in lexicographical order) for which values are returned. +This means that if an `end` is specified, it must be smaller than `start`, +otherwise no values will be returned. + +Example query: + +``` +GET /my_bucket HTTP/1.1 +``` + +Example response: + +```json +HTTP/1.1 200 OK + +{ + prefix: null, + start: null, + end: null, + limit: null, + reverse: false, + partitionKeys: [ + { pk: "keys", n: 3043 }, + { pk: "mailbox:INBOX", n: 42 }, + { pk: "mailbox:Junk", n: 2991 }, + { pk: "mailbox:Trash", n: 10 }, + { pk: "mailboxes", n: 3 }, + ], + more: false, + nextStart: null, +} +``` + + +### Operations on batches of items + +**InsertBatch: `POST /`** + +Simple insertion and deletion of triplets. The body is just a list of items to +insert in the following format: +`{ pk: "", sk: "", ct: ""|null, v: ""|null }`. + +The causality token should be the one returned in a previous read request (e.g. +by ReadItem or ReadBatch), to indicate that this write takes into account the +values that were returned from these reads, and supersedes them causally. If +the triplet is inserted for the first time, the causality token should be set to +`null`. + +The value is expected to be a base64-encoded binary blob. The value `null` can +also be used to delete the triplet while preserving causality information: this +allows to know if a delete has happenned concurrently with an insert, in which +case both are preserved and returned on reads (see below). + +Partition keys and sort keys are utf8 strings which are stored sorted by +lexicographical ordering of their binary representation. + +Example query: + +```json +POST /my_bucket HTTP/1.1 + +[ + { pk: "mailbox:INBOX", sk: "001892831", ct: "opaquetoken321", v: "b64cryptoblob321updated" }, + { pk: "mailbox:INBOX", sk: "001892912", ct: null, v: "b64cryptoblob444" }, + { pk: "mailbox:INBOX", sk: "001892932", ct: "opaquetoken654", v: null }, +] +``` + +Example response: + +``` +HTTP/1.1 200 OK +``` + + +**ReadBatch: `POST /?search`**, or alternatively
+**ReadBatch: `SEARCH /`** + +Batch read of triplets in a bucket. + +The request body is a JSON list of searches, that each specify a range of +items to get (to get single items, set `singleItem` to `true`). A search is a +JSON struct with the following fields: + +| name | default value | meaning | +| - | - | - | +| `partitionKey` | **mandatory** | The partition key in which to search | +| `prefix` | `null` | Restrict items to list to those whose sort keys start with this prefix | +| `start` | `null` | The sort key of the first item to read | +| `end` | `null` | The sort key of the last item to read (excluded) | +| `limit` | `null` | The maximum number of items to return | +| `reverse` | `false` | Iterate in reverse lexicographical order on sort keys | +| `singleItem` | `false` | Whether to return only the item with sort key `start` | +| `conflictsOnly` | `false` | Whether to return only items that have several concurrent values | +| `tombstones` | `false` | Whether or not to return tombstone lines to indicate the presence of old deleted items | + + +For each of the searches, triplets are listed and returned separately. The +semantics of `prefix`, `start`, `end`, `limit` and `reverse` are the same as for ReadIndex. The +additionnal parameter `singleItem` allows to get a single item, whose sort key +is the one given in `start`. Parameters `conflictsOnly` and `tombstones` +control additional filters on the items that are returned. + +The result is a list of length the number of searches, that consists in for +each search a JSON object specified similarly to the result of ReadIndex, but +that lists triplets within a partition key. + +The format of returned tuples is as follows: `{ sk: "", ct: "", v: ["", ...] }`, with the following fields: + +- `sk` (sort key): any unicode string used as a sort key + +- `ct` (causality token): an opaque token served by the server (generally + base64-encoded) to be used in subsequent writes to this key + +- `v` (list of values): each value is a binary blob, always base64-encoded; + contains multiple items when concurrent values exists + +- in case of concurrent update and deletion, a `null` is added to the list of concurrent values + +- if the `tombstones` query parameter is set to `true`, tombstones are returned + for items that have been deleted (this can be usefull for inserting after an + item that has been deleted, so that the insert is not considered + concurrent with the delete). Tombstones are returned as tuples in the + same format with only `null` values + +Example query: + +```json +POST /my_bucket?search HTTP/1.1 + +[ + { + partitionKey: "mailboxes", + }, + { + partitionKey: "mailbox:INBOX", + start: "001892831", + limit: 3, + }, + { + partitionKey: "keys", + start: "0", + singleItem: true, + }, +] +``` + +Example associated response body: + +```json +HTTP/1.1 200 OK + +[ + { + partitionKey: "mailboxes", + prefix: null, + start: null, + end: null, + limit: null, + reverse: false, + conflictsOnly: false, + tombstones: false, + singleItem: false, + items: [ + { sk: "INBOX", ct: "opaquetoken123", v: ["b64cryptoblob123", "b64cryptoblob'123"] }, + { sk: "Trash", ct: "opaquetoken456", v: ["b64cryptoblob456"] }, + { sk: "Junk", ct: "opaquetoken789", v: ["b64cryptoblob789"] }, + ], + more: false, + nextStart: null, + }, + { + partitionKey: "mailbox::INBOX", + prefix: null, + start: "001892831", + end: null, + limit: 3, + reverse: false, + conflictsOnly: false, + tombstones: false, + singleItem: false, + items: [ + { sk: "001892831", ct: "opaquetoken321", v: ["b64cryptoblob321"] }, + { sk: "001892832", ct: "opaquetoken654", v: ["b64cryptoblob654"] }, + { sk: "001892874", ct: "opaquetoken987", v: ["b64cryptoblob987"] }, + ], + more: true, + nextStart: "001892898", + }, + { + partitionKey: "keys", + prefix: null, + start: "0", + end: null, + conflictsOnly: false, + tombstones: false, + limit: null, + reverse: false, + singleItem: true, + items: [ + { sk: "0", ct: "opaquetoken999", v: ["b64binarystuff999"] }, + ], + more: false, + nextStart: null, + }, +] +``` + + + +**DeleteBatch: `POST /?delete`** + +Batch deletion of triplets. The request format is the same for `POST +/?search` to indicate items or range of items, except that here they +are deleted instead of returned, but only the fields `partitionKey`, `prefix`, `start`, +`end`, and `singleItem` are supported. Causality information is not given by +the user: this request will internally list all triplets and write deletion +markers that supersede all of the versions that have been read. + +This request returns for each series of items to be deleted, the number of +matching items that have been found and deleted. + +Example query: + +```json +POST /my_bucket?delete HTTP/1.1 + +[ + { + partitionKey: "mailbox:OldMailbox", + }, + { + partitionKey: "mailbox:INBOX", + start: "0018928321", + singleItem: true, + }, +] +``` + +Example response: + +``` +HTTP/1.1 200 OK + +[ + { + partitionKey: "mailbox:OldMailbox", + prefix: null, + start: null, + end: null, + singleItem: false, + deletedItems: 35, + }, + { + partitionKey: "mailbox:INBOX", + prefix: null, + start: "0018928321", + end: null, + singleItem: true, + deletedItems: 1, + }, +] +``` + + +## Internals: causality tokens + +The method used is based on DVVS (dotted version vector sets). See: + +- the paper "Scalable and Accurate Causality Tracking for Eventually Consistent Data Stores" +- + +For DVVS to work, write operations (at each node) must take a lock on the data table. diff --git a/k2v_test.py b/k2v_test.py new file mode 100755 index 00000000..3219056e --- /dev/null +++ b/k2v_test.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python + +import os +import requests +from datetime import datetime + +# let's talk to our AWS Elasticsearch cluster +#from requests_aws4auth import AWS4Auth +#auth = AWS4Auth('GK31c2f218a2e44f485b94239e', +# 'b892c0665f0ada8a4755dae98baa3b133590e11dae3bcc1f9d769d67f16c3835', +# 'us-east-1', +# 's3') + +from aws_requests_auth.aws_auth import AWSRequestsAuth +auth = AWSRequestsAuth(aws_access_key='GK31c2f218a2e44f485b94239e', + aws_secret_access_key='b892c0665f0ada8a4755dae98baa3b133590e11dae3bcc1f9d769d67f16c3835', + aws_host='localhost:3812', + aws_region='us-east-1', + aws_service='k2v') + + +print("-- ReadIndex") +response = requests.get('http://localhost:3812/alex', + auth=auth) +print(response.headers) +print(response.text) + + +sort_keys = ["a", "b", "c", "d"] + +for sk in sort_keys: + print("-- (%s) Put initial (no CT)"%sk) + response = requests.put('http://localhost:3812/alex/root?sort_key=%s'%sk, + auth=auth, + data='{}: Hello, world!'.format(datetime.timestamp(datetime.now()))) + print(response.headers) + print(response.text) + + print("-- Get") + response = requests.get('http://localhost:3812/alex/root?sort_key=%s'%sk, + auth=auth) + print(response.headers) + print(response.text) + ct = response.headers["x-garage-causality-token"] + + print("-- ReadIndex") + response = requests.get('http://localhost:3812/alex', + auth=auth) + print(response.headers) + print(response.text) + + print("-- Put with CT") + response = requests.put('http://localhost:3812/alex/root?sort_key=%s'%sk, + auth=auth, + headers={'x-garage-causality-token': ct}, + data='{}: Good bye, world!'.format(datetime.timestamp(datetime.now()))) + print(response.headers) + print(response.text) + + print("-- Get") + response = requests.get('http://localhost:3812/alex/root?sort_key=%s'%sk, + auth=auth) + print(response.headers) + print(response.text) + + print("-- Put again with same CT (concurrent)") + response = requests.put('http://localhost:3812/alex/root?sort_key=%s'%sk, + auth=auth, + headers={'x-garage-causality-token': ct}, + data='{}: Concurrent value, oops'.format(datetime.timestamp(datetime.now()))) + print(response.headers) + print(response.text) + +for sk in sort_keys: + print("-- (%s) Get"%sk) + response = requests.get('http://localhost:3812/alex/root?sort_key=%s'%sk, + auth=auth) + print(response.headers) + print(response.text) + ct = response.headers["x-garage-causality-token"] + + print("-- Delete") + response = requests.delete('http://localhost:3812/alex/root?sort_key=%s'%sk, + headers={'x-garage-causality-token': ct}, + auth=auth) + print(response.headers) + print(response.text) + +print("-- ReadIndex") +response = requests.get('http://localhost:3812/alex', + auth=auth) +print(response.headers) +print(response.text) + +print("-- InsertBatch") +response = requests.post('http://localhost:3812/alex', + auth=auth, + data=''' +[ + {"pk": "root", "sk": "a", "ct": null, "v": "aW5pdGlhbCB0ZXN0Cg=="}, + {"pk": "root", "sk": "b", "ct": null, "v": "aW5pdGlhbCB0ZXN1Cg=="}, + {"pk": "root", "sk": "c", "ct": null, "v": "aW5pdGlhbCB0ZXN2Cg=="} +] +''') +print(response.headers) +print(response.text) + +print("-- ReadIndex") +response = requests.get('http://localhost:3812/alex', + auth=auth) +print(response.headers) +print(response.text) + +for sk in sort_keys: + print("-- (%s) Get"%sk) + response = requests.get('http://localhost:3812/alex/root?sort_key=%s'%sk, + auth=auth) + print(response.headers) + print(response.text) + ct = response.headers["x-garage-causality-token"] + +print("-- ReadBatch") +response = requests.post('http://localhost:3812/alex?search', + auth=auth, + data=''' +[ + {"partitionKey": "root"}, + {"partitionKey": "root", "tombstones": true}, + {"partitionKey": "root", "tombstones": true, "limit": 2}, + {"partitionKey": "root", "start": "c", "singleItem": true}, + {"partitionKey": "root", "start": "b", "end": "d", "tombstones": true} +] +''') +print(response.headers) +print(response.text) + + +print("-- DeleteBatch") +response = requests.post('http://localhost:3812/alex?delete', + auth=auth, + data=''' +[ + {"partitionKey": "root", "start": "b", "end": "c"} +] +''') +print(response.headers) +print(response.text) + +print("-- ReadBatch") +response = requests.post('http://localhost:3812/alex?search', + auth=auth, + data=''' +[ + {"partitionKey": "root"} +] +''') +print(response.headers) +print(response.text) diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml index 5e96b081..29b26e5e 100644 --- a/src/api/Cargo.toml +++ b/src/api/Cargo.toml @@ -18,7 +18,9 @@ garage_model = { version = "0.7.0", path = "../model" } garage_table = { version = "0.7.0", path = "../table" } garage_block = { version = "0.7.0", path = "../block" } garage_util = { version = "0.7.0", path = "../util" } +garage_rpc = { version = "0.7.0", path = "../rpc" } +async-trait = "0.1.7" base64 = "0.13" bytes = "1.0" chrono = "0.4" @@ -52,3 +54,6 @@ quick-xml = { version = "0.21", features = [ "serialize" ] } url = "2.1" opentelemetry = "0.17" + +[features] +k2v = [ "garage_util/k2v", "garage_model/k2v" ] diff --git a/src/api/api_server.rs b/src/api/api_server.rs deleted file mode 100644 index e7b86d9e..00000000 --- a/src/api/api_server.rs +++ /dev/null @@ -1,645 +0,0 @@ -use std::net::SocketAddr; -use std::sync::Arc; - -use chrono::{DateTime, NaiveDateTime, Utc}; -use futures::future::Future; -use futures::prelude::*; -use hyper::header; -use hyper::server::conn::AddrStream; -use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Method, Request, Response, Server}; - -use opentelemetry::{ - global, - metrics::{Counter, ValueRecorder}, - trace::{FutureExt, TraceContextExt, Tracer}, - Context, KeyValue, -}; - -use garage_util::data::*; -use garage_util::error::Error as GarageError; -use garage_util::metrics::{gen_trace_id, RecordDuration}; - -use garage_model::garage::Garage; -use garage_model::key_table::Key; - -use garage_table::util::*; - -use crate::error::*; -use crate::signature::compute_scope; -use crate::signature::payload::check_payload_signature; -use crate::signature::streaming::SignedPayloadStream; -use crate::signature::LONG_DATETIME; - -use crate::helpers::*; -use crate::s3_bucket::*; -use crate::s3_copy::*; -use crate::s3_cors::*; -use crate::s3_delete::*; -use crate::s3_get::*; -use crate::s3_list::*; -use crate::s3_post_object::handle_post_object; -use crate::s3_put::*; -use crate::s3_router::{Authorization, Endpoint}; -use crate::s3_website::*; - -struct ApiMetrics { - request_counter: Counter, - error_counter: Counter, - request_duration: ValueRecorder, -} - -impl ApiMetrics { - fn new() -> Self { - let meter = global::meter("garage/api"); - Self { - request_counter: meter - .u64_counter("api.request_counter") - .with_description("Number of API calls to the various S3 API endpoints") - .init(), - error_counter: meter - .u64_counter("api.error_counter") - .with_description( - "Number of API calls to the various S3 API endpoints that resulted in errors", - ) - .init(), - request_duration: meter - .f64_value_recorder("api.request_duration") - .with_description("Duration of API calls to the various S3 API endpoints") - .init(), - } - } -} - -/// Run the S3 API server -pub async fn run_api_server( - garage: Arc, - shutdown_signal: impl Future, -) -> Result<(), GarageError> { - let addr = &garage.config.s3_api.api_bind_addr; - - let metrics = Arc::new(ApiMetrics::new()); - - let service = make_service_fn(|conn: &AddrStream| { - let garage = garage.clone(); - let metrics = metrics.clone(); - - let client_addr = conn.remote_addr(); - async move { - Ok::<_, GarageError>(service_fn(move |req: Request| { - let garage = garage.clone(); - let metrics = metrics.clone(); - - handler(garage, metrics, req, client_addr) - })) - } - }); - - let server = Server::bind(addr).serve(service); - - let graceful = server.with_graceful_shutdown(shutdown_signal); - info!("API server listening on http://{}", addr); - - graceful.await?; - Ok(()) -} - -async fn handler( - garage: Arc, - metrics: Arc, - req: Request, - addr: SocketAddr, -) -> Result, GarageError> { - let uri = req.uri().clone(); - info!("{} {} {}", addr, req.method(), uri); - debug!("{:?}", req); - - let tracer = opentelemetry::global::tracer("garage"); - let span = tracer - .span_builder("S3 API call (unknown)") - .with_trace_id(gen_trace_id()) - .with_attributes(vec![ - KeyValue::new("method", format!("{}", req.method())), - KeyValue::new("uri", req.uri().to_string()), - ]) - .start(&tracer); - - let res = handler_stage2(garage.clone(), metrics, req) - .with_context(Context::current_with_span(span)) - .await; - - match res { - Ok(x) => { - debug!("{} {:?}", x.status(), x.headers()); - Ok(x) - } - Err(e) => { - let body: Body = Body::from(e.aws_xml(&garage.config.s3_api.s3_region, uri.path())); - let mut http_error_builder = Response::builder() - .status(e.http_status_code()) - .header("Content-Type", "application/xml"); - - if let Some(header_map) = http_error_builder.headers_mut() { - e.add_headers(header_map) - } - - let http_error = http_error_builder.body(body)?; - - if e.http_status_code().is_server_error() { - warn!("Response: error {}, {}", e.http_status_code(), e); - } else { - info!("Response: error {}, {}", e.http_status_code(), e); - } - Ok(http_error) - } - } -} - -async fn handler_stage2( - garage: Arc, - metrics: Arc, - req: Request, -) -> Result, Error> { - let authority = req - .headers() - .get(header::HOST) - .ok_or_bad_request("Host header required")? - .to_str()?; - - let host = authority_to_host(authority)?; - - let bucket_name = garage - .config - .s3_api - .root_domain - .as_ref() - .and_then(|root_domain| host_to_bucket(&host, root_domain)); - - let (endpoint, bucket_name) = Endpoint::from_request(&req, bucket_name.map(ToOwned::to_owned))?; - debug!("Endpoint: {:?}", endpoint); - - let current_context = Context::current(); - let current_span = current_context.span(); - current_span.update_name::(format!("S3 API {}", endpoint.name())); - current_span.set_attribute(KeyValue::new("endpoint", endpoint.name())); - current_span.set_attribute(KeyValue::new( - "bucket", - bucket_name.clone().unwrap_or_default(), - )); - - let metrics_tags = &[KeyValue::new("api_endpoint", endpoint.name())]; - - let res = handler_stage3(garage, req, endpoint, bucket_name) - .record_duration(&metrics.request_duration, &metrics_tags[..]) - .await; - - metrics.request_counter.add(1, &metrics_tags[..]); - - let status_code = match &res { - Ok(r) => r.status(), - Err(e) => e.http_status_code(), - }; - if status_code.is_client_error() || status_code.is_server_error() { - metrics.error_counter.add( - 1, - &[ - metrics_tags[0].clone(), - KeyValue::new("status_code", status_code.as_str().to_string()), - ], - ); - } - - res -} - -async fn handler_stage3( - garage: Arc, - req: Request, - endpoint: Endpoint, - bucket_name: Option, -) -> Result, Error> { - // Some endpoints are processed early, before we even check for an API key - if let Endpoint::PostObject = endpoint { - return handle_post_object(garage, req, bucket_name.unwrap()).await; - } - if let Endpoint::Options = endpoint { - return handle_options_s3api(garage, &req, bucket_name).await; - } - - let (api_key, mut content_sha256) = check_payload_signature(&garage, &req).await?; - let api_key = api_key.ok_or_else(|| { - Error::Forbidden("Garage does not support anonymous access yet".to_string()) - })?; - - let req = match req.headers().get("x-amz-content-sha256") { - Some(header) if header == "STREAMING-AWS4-HMAC-SHA256-PAYLOAD" => { - let signature = content_sha256 - .take() - .ok_or_bad_request("No signature provided")?; - - let secret_key = &api_key - .state - .as_option() - .ok_or_internal_error("Deleted key state")? - .secret_key; - - let date = req - .headers() - .get("x-amz-date") - .ok_or_bad_request("Missing X-Amz-Date field")? - .to_str()?; - let date: NaiveDateTime = NaiveDateTime::parse_from_str(date, LONG_DATETIME) - .ok_or_bad_request("Invalid date")?; - let date: DateTime = DateTime::from_utc(date, Utc); - - let scope = compute_scope(&date, &garage.config.s3_api.s3_region); - let signing_hmac = crate::signature::signing_hmac( - &date, - secret_key, - &garage.config.s3_api.s3_region, - "s3", - ) - .ok_or_internal_error("Unable to build signing HMAC")?; - - req.map(move |body| { - Body::wrap_stream( - SignedPayloadStream::new( - body.map_err(Error::from), - signing_hmac, - date, - &scope, - signature, - ) - .map_err(Error::from), - ) - }) - } - _ => req, - }; - - let bucket_name = match bucket_name { - None => return handle_request_without_bucket(garage, req, api_key, endpoint).await, - Some(bucket) => bucket.to_string(), - }; - - // Special code path for CreateBucket API endpoint - if let Endpoint::CreateBucket {} = endpoint { - return handle_create_bucket(&garage, req, content_sha256, api_key, bucket_name).await; - } - - let bucket_id = resolve_bucket(&garage, &bucket_name, &api_key).await?; - let bucket = garage - .bucket_table - .get(&EmptyKey, &bucket_id) - .await? - .filter(|b| !b.state.is_deleted()) - .ok_or(Error::NoSuchBucket)?; - - let allowed = match endpoint.authorization_type() { - Authorization::Read => api_key.allow_read(&bucket_id), - Authorization::Write => api_key.allow_write(&bucket_id), - Authorization::Owner => api_key.allow_owner(&bucket_id), - _ => unreachable!(), - }; - - if !allowed { - return Err(Error::Forbidden( - "Operation is not allowed for this key.".to_string(), - )); - } - - // Look up what CORS rule might apply to response. - // Requests for methods different than GET, HEAD or POST - // are always preflighted, i.e. the browser should make - // an OPTIONS call before to check it is allowed - let matching_cors_rule = match *req.method() { - Method::GET | Method::HEAD | Method::POST => find_matching_cors_rule(&bucket, &req)?, - _ => None, - }; - - let resp = match endpoint { - Endpoint::HeadObject { - key, part_number, .. - } => handle_head(garage, &req, bucket_id, &key, part_number).await, - Endpoint::GetObject { - key, part_number, .. - } => handle_get(garage, &req, bucket_id, &key, part_number).await, - Endpoint::UploadPart { - key, - part_number, - upload_id, - } => { - handle_put_part( - garage, - req, - bucket_id, - &key, - part_number, - &upload_id, - content_sha256, - ) - .await - } - Endpoint::CopyObject { key } => handle_copy(garage, &api_key, &req, bucket_id, &key).await, - Endpoint::UploadPartCopy { - key, - part_number, - upload_id, - } => { - handle_upload_part_copy( - garage, - &api_key, - &req, - bucket_id, - &key, - part_number, - &upload_id, - ) - .await - } - Endpoint::PutObject { key } => { - handle_put(garage, req, bucket_id, &key, content_sha256).await - } - Endpoint::AbortMultipartUpload { key, upload_id } => { - handle_abort_multipart_upload(garage, bucket_id, &key, &upload_id).await - } - Endpoint::DeleteObject { key, .. } => handle_delete(garage, bucket_id, &key).await, - Endpoint::CreateMultipartUpload { key } => { - handle_create_multipart_upload(garage, &req, &bucket_name, bucket_id, &key).await - } - Endpoint::CompleteMultipartUpload { key, upload_id } => { - handle_complete_multipart_upload( - garage, - req, - &bucket_name, - bucket_id, - &key, - &upload_id, - content_sha256, - ) - .await - } - Endpoint::CreateBucket {} => unreachable!(), - Endpoint::HeadBucket {} => { - let empty_body: Body = Body::from(vec![]); - let response = Response::builder().body(empty_body).unwrap(); - Ok(response) - } - Endpoint::DeleteBucket {} => { - handle_delete_bucket(&garage, bucket_id, bucket_name, api_key).await - } - Endpoint::GetBucketLocation {} => handle_get_bucket_location(garage), - Endpoint::GetBucketVersioning {} => handle_get_bucket_versioning(), - Endpoint::ListObjects { - delimiter, - encoding_type, - marker, - max_keys, - prefix, - } => { - handle_list( - garage, - &ListObjectsQuery { - common: ListQueryCommon { - bucket_name, - bucket_id, - delimiter: delimiter.map(|d| d.to_string()), - page_size: max_keys.map(|p| p.clamp(1, 1000)).unwrap_or(1000), - prefix: prefix.unwrap_or_default(), - urlencode_resp: encoding_type.map(|e| e == "url").unwrap_or(false), - }, - is_v2: false, - marker, - continuation_token: None, - start_after: None, - }, - ) - .await - } - Endpoint::ListObjectsV2 { - delimiter, - encoding_type, - max_keys, - prefix, - continuation_token, - start_after, - list_type, - .. - } => { - if list_type == "2" { - handle_list( - garage, - &ListObjectsQuery { - common: ListQueryCommon { - bucket_name, - bucket_id, - delimiter: delimiter.map(|d| d.to_string()), - page_size: max_keys.map(|p| p.clamp(1, 1000)).unwrap_or(1000), - urlencode_resp: encoding_type.map(|e| e == "url").unwrap_or(false), - prefix: prefix.unwrap_or_default(), - }, - is_v2: true, - marker: None, - continuation_token, - start_after, - }, - ) - .await - } else { - Err(Error::BadRequest(format!( - "Invalid endpoint: list-type={}", - list_type - ))) - } - } - Endpoint::ListMultipartUploads { - delimiter, - encoding_type, - key_marker, - max_uploads, - prefix, - upload_id_marker, - } => { - handle_list_multipart_upload( - garage, - &ListMultipartUploadsQuery { - common: ListQueryCommon { - bucket_name, - bucket_id, - delimiter: delimiter.map(|d| d.to_string()), - page_size: max_uploads.map(|p| p.clamp(1, 1000)).unwrap_or(1000), - prefix: prefix.unwrap_or_default(), - urlencode_resp: encoding_type.map(|e| e == "url").unwrap_or(false), - }, - key_marker, - upload_id_marker, - }, - ) - .await - } - Endpoint::ListParts { - key, - max_parts, - part_number_marker, - upload_id, - } => { - handle_list_parts( - garage, - &ListPartsQuery { - bucket_name, - bucket_id, - key, - upload_id, - part_number_marker: part_number_marker.map(|p| p.clamp(1, 10000)), - max_parts: max_parts.map(|p| p.clamp(1, 1000)).unwrap_or(1000), - }, - ) - .await - } - Endpoint::DeleteObjects {} => { - handle_delete_objects(garage, bucket_id, req, content_sha256).await - } - Endpoint::GetBucketWebsite {} => handle_get_website(&bucket).await, - Endpoint::PutBucketWebsite {} => { - handle_put_website(garage, bucket_id, req, content_sha256).await - } - Endpoint::DeleteBucketWebsite {} => handle_delete_website(garage, bucket_id).await, - Endpoint::GetBucketCors {} => handle_get_cors(&bucket).await, - Endpoint::PutBucketCors {} => handle_put_cors(garage, bucket_id, req, content_sha256).await, - Endpoint::DeleteBucketCors {} => handle_delete_cors(garage, bucket_id).await, - endpoint => Err(Error::NotImplemented(endpoint.name().to_owned())), - }; - - // If request was a success and we have a CORS rule that applies to it, - // add the corresponding CORS headers to the response - let mut resp_ok = resp?; - if let Some(rule) = matching_cors_rule { - add_cors_headers(&mut resp_ok, rule) - .ok_or_internal_error("Invalid bucket CORS configuration")?; - } - - Ok(resp_ok) -} - -async fn handle_request_without_bucket( - garage: Arc, - _req: Request, - api_key: Key, - endpoint: Endpoint, -) -> Result, Error> { - match endpoint { - Endpoint::ListBuckets => handle_list_buckets(&garage, &api_key).await, - endpoint => Err(Error::NotImplemented(endpoint.name().to_owned())), - } -} - -#[allow(clippy::ptr_arg)] -pub async fn resolve_bucket( - garage: &Garage, - bucket_name: &String, - api_key: &Key, -) -> Result { - let api_key_params = api_key - .state - .as_option() - .ok_or_internal_error("Key should not be deleted at this point")?; - - if let Some(Some(bucket_id)) = api_key_params.local_aliases.get(bucket_name) { - Ok(*bucket_id) - } else { - Ok(garage - .bucket_helper() - .resolve_global_bucket_name(bucket_name) - .await? - .ok_or(Error::NoSuchBucket)?) - } -} - -/// Extract the bucket name and the key name from an HTTP path and possibly a bucket provided in -/// the host header of the request -/// -/// S3 internally manages only buckets and keys. This function splits -/// an HTTP path to get the corresponding bucket name and key. -pub fn parse_bucket_key<'a>( - path: &'a str, - host_bucket: Option<&'a str>, -) -> Result<(&'a str, Option<&'a str>), Error> { - let path = path.trim_start_matches('/'); - - if let Some(bucket) = host_bucket { - if !path.is_empty() { - return Ok((bucket, Some(path))); - } else { - return Ok((bucket, None)); - } - } - - let (bucket, key) = match path.find('/') { - Some(i) => { - let key = &path[i + 1..]; - if !key.is_empty() { - (&path[..i], Some(key)) - } else { - (&path[..i], None) - } - } - None => (path, None), - }; - if bucket.is_empty() { - return Err(Error::BadRequest("No bucket specified".to_string())); - } - Ok((bucket, key)) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_bucket_containing_a_key() -> Result<(), Error> { - let (bucket, key) = parse_bucket_key("/my_bucket/a/super/file.jpg", None)?; - assert_eq!(bucket, "my_bucket"); - assert_eq!(key.expect("key must be set"), "a/super/file.jpg"); - Ok(()) - } - - #[test] - fn parse_bucket_containing_no_key() -> Result<(), Error> { - let (bucket, key) = parse_bucket_key("/my_bucket/", None)?; - assert_eq!(bucket, "my_bucket"); - assert!(key.is_none()); - let (bucket, key) = parse_bucket_key("/my_bucket", None)?; - assert_eq!(bucket, "my_bucket"); - assert!(key.is_none()); - Ok(()) - } - - #[test] - fn parse_bucket_containing_no_bucket() { - let parsed = parse_bucket_key("", None); - assert!(parsed.is_err()); - let parsed = parse_bucket_key("/", None); - assert!(parsed.is_err()); - let parsed = parse_bucket_key("////", None); - assert!(parsed.is_err()); - } - - #[test] - fn parse_bucket_with_vhost_and_key() -> Result<(), Error> { - let (bucket, key) = parse_bucket_key("/a/super/file.jpg", Some("my-bucket"))?; - assert_eq!(bucket, "my-bucket"); - assert_eq!(key.expect("key must be set"), "a/super/file.jpg"); - Ok(()) - } - - #[test] - fn parse_bucket_with_vhost_no_key() -> Result<(), Error> { - let (bucket, key) = parse_bucket_key("", Some("my-bucket"))?; - assert_eq!(bucket, "my-bucket"); - assert!(key.is_none()); - let (bucket, key) = parse_bucket_key("/", Some("my-bucket"))?; - assert_eq!(bucket, "my-bucket"); - assert!(key.is_none()); - Ok(()) - } -} diff --git a/src/api/error.rs b/src/api/error.rs index f53ed1fd..4b7254d2 100644 --- a/src/api/error.rs +++ b/src/api/error.rs @@ -7,7 +7,7 @@ use hyper::{HeaderMap, StatusCode}; use garage_model::helper::error::Error as HelperError; use garage_util::error::Error as GarageError; -use crate::s3_xml; +use crate::s3::xml as s3_xml; /// Errors of this crate #[derive(Debug, Error)] @@ -100,6 +100,10 @@ pub enum Error { #[error(display = "Bad request: {}", _0)] BadRequest(String), + /// The client asked for an invalid return format (invalid Accept header) + #[error(display = "Not acceptable: {}", _0)] + NotAcceptable(String), + /// The client sent a request for an action not supported by garage #[error(display = "Unimplemented action: {}", _0)] NotImplemented(String), @@ -140,6 +144,7 @@ impl Error { Error::BucketNotEmpty | Error::BucketAlreadyExists => StatusCode::CONFLICT, Error::PreconditionFailed => StatusCode::PRECONDITION_FAILED, Error::Forbidden(_) => StatusCode::FORBIDDEN, + Error::NotAcceptable(_) => StatusCode::NOT_ACCEPTABLE, Error::InternalError( GarageError::Timeout | GarageError::RemoteError(_) diff --git a/src/api/generic_server.rs b/src/api/generic_server.rs new file mode 100644 index 00000000..9281e596 --- /dev/null +++ b/src/api/generic_server.rs @@ -0,0 +1,202 @@ +use std::net::SocketAddr; +use std::sync::Arc; + +use async_trait::async_trait; + +use futures::future::Future; + +use hyper::server::conn::AddrStream; +use hyper::service::{make_service_fn, service_fn}; +use hyper::{Body, Request, Response, Server}; + +use opentelemetry::{ + global, + metrics::{Counter, ValueRecorder}, + trace::{FutureExt, SpanRef, TraceContextExt, Tracer}, + Context, KeyValue, +}; + +use garage_util::error::Error as GarageError; +use garage_util::metrics::{gen_trace_id, RecordDuration}; + +use crate::error::*; + +pub(crate) trait ApiEndpoint: Send + Sync + 'static { + fn name(&self) -> &'static str; + fn add_span_attributes(&self, span: SpanRef<'_>); +} + +#[async_trait] +pub(crate) trait ApiHandler: Send + Sync + 'static { + const API_NAME: &'static str; + const API_NAME_DISPLAY: &'static str; + + type Endpoint: ApiEndpoint; + + fn parse_endpoint(&self, r: &Request) -> Result; + async fn handle( + &self, + req: Request, + endpoint: Self::Endpoint, + ) -> Result, Error>; +} + +pub(crate) struct ApiServer { + region: String, + api_handler: A, + + // Metrics + request_counter: Counter, + error_counter: Counter, + request_duration: ValueRecorder, +} + +impl ApiServer { + pub fn new(region: String, api_handler: A) -> Arc { + let meter = global::meter("garage/api"); + Arc::new(Self { + region, + api_handler, + request_counter: meter + .u64_counter(format!("api.{}.request_counter", A::API_NAME)) + .with_description(format!( + "Number of API calls to the various {} API endpoints", + A::API_NAME_DISPLAY + )) + .init(), + error_counter: meter + .u64_counter(format!("api.{}.error_counter", A::API_NAME)) + .with_description(format!( + "Number of API calls to the various {} API endpoints that resulted in errors", + A::API_NAME_DISPLAY + )) + .init(), + request_duration: meter + .f64_value_recorder(format!("api.{}.request_duration", A::API_NAME)) + .with_description(format!( + "Duration of API calls to the various {} API endpoints", + A::API_NAME_DISPLAY + )) + .init(), + }) + } + + pub async fn run_server( + self: Arc, + bind_addr: SocketAddr, + shutdown_signal: impl Future, + ) -> Result<(), GarageError> { + let service = make_service_fn(|conn: &AddrStream| { + let this = self.clone(); + + let client_addr = conn.remote_addr(); + async move { + Ok::<_, GarageError>(service_fn(move |req: Request| { + let this = this.clone(); + + this.handler(req, client_addr) + })) + } + }); + + let server = Server::bind(&bind_addr).serve(service); + + let graceful = server.with_graceful_shutdown(shutdown_signal); + info!( + "{} API server listening on http://{}", + A::API_NAME_DISPLAY, + bind_addr + ); + + graceful.await?; + Ok(()) + } + + async fn handler( + self: Arc, + req: Request, + addr: SocketAddr, + ) -> Result, GarageError> { + let uri = req.uri().clone(); + info!("{} {} {}", addr, req.method(), uri); + debug!("{:?}", req); + + let tracer = opentelemetry::global::tracer("garage"); + let span = tracer + .span_builder(format!("{} API call (unknown)", A::API_NAME_DISPLAY)) + .with_trace_id(gen_trace_id()) + .with_attributes(vec![ + KeyValue::new("method", format!("{}", req.method())), + KeyValue::new("uri", req.uri().to_string()), + ]) + .start(&tracer); + + let res = self + .handler_stage2(req) + .with_context(Context::current_with_span(span)) + .await; + + match res { + Ok(x) => { + debug!("{} {:?}", x.status(), x.headers()); + Ok(x) + } + Err(e) => { + let body: Body = Body::from(e.aws_xml(&self.region, uri.path())); + let mut http_error_builder = Response::builder() + .status(e.http_status_code()) + .header("Content-Type", "application/xml"); + + if let Some(header_map) = http_error_builder.headers_mut() { + e.add_headers(header_map) + } + + let http_error = http_error_builder.body(body)?; + + if e.http_status_code().is_server_error() { + warn!("Response: error {}, {}", e.http_status_code(), e); + } else { + info!("Response: error {}, {}", e.http_status_code(), e); + } + Ok(http_error) + } + } + } + + async fn handler_stage2(&self, req: Request) -> Result, Error> { + let endpoint = self.api_handler.parse_endpoint(&req)?; + debug!("Endpoint: {}", endpoint.name()); + + let current_context = Context::current(); + let current_span = current_context.span(); + current_span.update_name::(format!("S3 API {}", endpoint.name())); + current_span.set_attribute(KeyValue::new("endpoint", endpoint.name())); + endpoint.add_span_attributes(current_span); + + let metrics_tags = &[KeyValue::new("api_endpoint", endpoint.name())]; + + let res = self + .api_handler + .handle(req, endpoint) + .record_duration(&self.request_duration, &metrics_tags[..]) + .await; + + self.request_counter.add(1, &metrics_tags[..]); + + let status_code = match &res { + Ok(r) => r.status(), + Err(e) => e.http_status_code(), + }; + if status_code.is_client_error() || status_code.is_server_error() { + self.error_counter.add( + 1, + &[ + metrics_tags[0].clone(), + KeyValue::new("status_code", status_code.as_str().to_string()), + ], + ); + } + + res + } +} diff --git a/src/api/helpers.rs b/src/api/helpers.rs index c2709bb3..a994b82f 100644 --- a/src/api/helpers.rs +++ b/src/api/helpers.rs @@ -1,6 +1,25 @@ -use crate::Error; use idna::domain_to_unicode; +use garage_util::data::*; + +use garage_model::garage::Garage; +use garage_model::key_table::Key; + +use crate::error::*; + +/// What kind of authorization is required to perform a given action +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Authorization { + /// No authorization is required + None, + /// Having Read permission on bucket + Read, + /// Having Write permission on bucket + Write, + /// Having Owner permission on bucket + Owner, +} + /// Host to bucket /// /// Convert a host, like "bucket.garage-site.tld" to the corresponding bucket "bucket", @@ -60,10 +79,142 @@ pub fn authority_to_host(authority: &str) -> Result { authority.map(|h| domain_to_unicode(h).0) } +#[allow(clippy::ptr_arg)] +pub async fn resolve_bucket( + garage: &Garage, + bucket_name: &String, + api_key: &Key, +) -> Result { + let api_key_params = api_key + .state + .as_option() + .ok_or_internal_error("Key should not be deleted at this point")?; + + if let Some(Some(bucket_id)) = api_key_params.local_aliases.get(bucket_name) { + Ok(*bucket_id) + } else { + Ok(garage + .bucket_helper() + .resolve_global_bucket_name(bucket_name) + .await? + .ok_or(Error::NoSuchBucket)?) + } +} + +/// Extract the bucket name and the key name from an HTTP path and possibly a bucket provided in +/// the host header of the request +/// +/// S3 internally manages only buckets and keys. This function splits +/// an HTTP path to get the corresponding bucket name and key. +pub fn parse_bucket_key<'a>( + path: &'a str, + host_bucket: Option<&'a str>, +) -> Result<(&'a str, Option<&'a str>), Error> { + let path = path.trim_start_matches('/'); + + if let Some(bucket) = host_bucket { + if !path.is_empty() { + return Ok((bucket, Some(path))); + } else { + return Ok((bucket, None)); + } + } + + let (bucket, key) = match path.find('/') { + Some(i) => { + let key = &path[i + 1..]; + if !key.is_empty() { + (&path[..i], Some(key)) + } else { + (&path[..i], None) + } + } + None => (path, None), + }; + if bucket.is_empty() { + return Err(Error::BadRequest("No bucket specified".to_string())); + } + Ok((bucket, key)) +} + +const UTF8_BEFORE_LAST_CHAR: char = '\u{10FFFE}'; + +/// Compute the key after the prefix +pub fn key_after_prefix(pfx: &str) -> Option { + let mut next = pfx.to_string(); + while !next.is_empty() { + let tail = next.pop().unwrap(); + if tail >= char::MAX { + continue; + } + + // Circumvent a limitation of RangeFrom that overflow earlier than needed + // See: https://doc.rust-lang.org/core/ops/struct.RangeFrom.html + let new_tail = if tail == UTF8_BEFORE_LAST_CHAR { + char::MAX + } else { + (tail..).nth(1).unwrap() + }; + + next.push(new_tail); + return Some(next); + } + + None +} + #[cfg(test)] mod tests { use super::*; + #[test] + fn parse_bucket_containing_a_key() -> Result<(), Error> { + let (bucket, key) = parse_bucket_key("/my_bucket/a/super/file.jpg", None)?; + assert_eq!(bucket, "my_bucket"); + assert_eq!(key.expect("key must be set"), "a/super/file.jpg"); + Ok(()) + } + + #[test] + fn parse_bucket_containing_no_key() -> Result<(), Error> { + let (bucket, key) = parse_bucket_key("/my_bucket/", None)?; + assert_eq!(bucket, "my_bucket"); + assert!(key.is_none()); + let (bucket, key) = parse_bucket_key("/my_bucket", None)?; + assert_eq!(bucket, "my_bucket"); + assert!(key.is_none()); + Ok(()) + } + + #[test] + fn parse_bucket_containing_no_bucket() { + let parsed = parse_bucket_key("", None); + assert!(parsed.is_err()); + let parsed = parse_bucket_key("/", None); + assert!(parsed.is_err()); + let parsed = parse_bucket_key("////", None); + assert!(parsed.is_err()); + } + + #[test] + fn parse_bucket_with_vhost_and_key() -> Result<(), Error> { + let (bucket, key) = parse_bucket_key("/a/super/file.jpg", Some("my-bucket"))?; + assert_eq!(bucket, "my-bucket"); + assert_eq!(key.expect("key must be set"), "a/super/file.jpg"); + Ok(()) + } + + #[test] + fn parse_bucket_with_vhost_no_key() -> Result<(), Error> { + let (bucket, key) = parse_bucket_key("", Some("my-bucket"))?; + assert_eq!(bucket, "my-bucket"); + assert!(key.is_none()); + let (bucket, key) = parse_bucket_key("/", Some("my-bucket"))?; + assert_eq!(bucket, "my-bucket"); + assert!(key.is_none()); + Ok(()) + } + #[test] fn authority_to_host_with_port() -> Result<(), Error> { let domain = authority_to_host("[::1]:3902")?; @@ -111,4 +262,39 @@ mod tests { assert_eq!(host_to_bucket("not-garage.tld", "garage.tld"), None); assert_eq!(host_to_bucket("not-garage.tld", ".garage.tld"), None); } + + #[test] + fn test_key_after_prefix() { + use std::iter::FromIterator; + + assert_eq!(UTF8_BEFORE_LAST_CHAR as u32, (char::MAX as u32) - 1); + assert_eq!(key_after_prefix("a/b/").unwrap().as_str(), "a/b0"); + assert_eq!(key_after_prefix("€").unwrap().as_str(), "₭"); + assert_eq!( + key_after_prefix("􏿽").unwrap().as_str(), + String::from(char::from_u32(0x10FFFE).unwrap()) + ); + + // When the last character is the biggest UTF8 char + let a = String::from_iter(['a', char::MAX].iter()); + assert_eq!(key_after_prefix(a.as_str()).unwrap().as_str(), "b"); + + // When all characters are the biggest UTF8 char + let b = String::from_iter([char::MAX; 3].iter()); + assert!(key_after_prefix(b.as_str()).is_none()); + + // Check utf8 surrogates + let c = String::from('\u{D7FF}'); + assert_eq!( + key_after_prefix(c.as_str()).unwrap().as_str(), + String::from('\u{E000}') + ); + + // Check the character before the biggest one + let d = String::from('\u{10FFFE}'); + assert_eq!( + key_after_prefix(d.as_str()).unwrap().as_str(), + String::from(char::MAX) + ); + } } diff --git a/src/api/k2v/api_server.rs b/src/api/k2v/api_server.rs new file mode 100644 index 00000000..5f5e9030 --- /dev/null +++ b/src/api/k2v/api_server.rs @@ -0,0 +1,195 @@ +use std::sync::Arc; + +use async_trait::async_trait; + +use futures::future::Future; +use hyper::{Body, Method, Request, Response}; + +use opentelemetry::{trace::SpanRef, KeyValue}; + +use garage_table::util::*; +use garage_util::error::Error as GarageError; + +use garage_model::garage::Garage; + +use crate::error::*; +use crate::generic_server::*; + +use crate::signature::payload::check_payload_signature; +use crate::signature::streaming::*; + +use crate::helpers::*; +use crate::k2v::batch::*; +use crate::k2v::index::*; +use crate::k2v::item::*; +use crate::k2v::router::Endpoint; +use crate::s3::cors::*; + +pub struct K2VApiServer { + garage: Arc, +} + +pub(crate) struct K2VApiEndpoint { + bucket_name: String, + endpoint: Endpoint, +} + +impl K2VApiServer { + pub async fn run( + garage: Arc, + shutdown_signal: impl Future, + ) -> Result<(), GarageError> { + if let Some(cfg) = &garage.config.k2v_api { + let bind_addr = cfg.api_bind_addr; + + ApiServer::new( + garage.config.s3_api.s3_region.clone(), + K2VApiServer { garage }, + ) + .run_server(bind_addr, shutdown_signal) + .await + } else { + Ok(()) + } + } +} + +#[async_trait] +impl ApiHandler for K2VApiServer { + const API_NAME: &'static str = "k2v"; + const API_NAME_DISPLAY: &'static str = "K2V"; + + type Endpoint = K2VApiEndpoint; + + fn parse_endpoint(&self, req: &Request) -> Result { + let (endpoint, bucket_name) = Endpoint::from_request(req)?; + + Ok(K2VApiEndpoint { + bucket_name, + endpoint, + }) + } + + async fn handle( + &self, + req: Request, + endpoint: K2VApiEndpoint, + ) -> Result, Error> { + let K2VApiEndpoint { + bucket_name, + endpoint, + } = endpoint; + let garage = self.garage.clone(); + + // The OPTIONS method is procesed early, before we even check for an API key + if let Endpoint::Options = endpoint { + return handle_options_s3api(garage, &req, Some(bucket_name)).await; + } + + let (api_key, mut content_sha256) = check_payload_signature(&garage, "k2v", &req).await?; + let api_key = api_key.ok_or_else(|| { + Error::Forbidden("Garage does not support anonymous access yet".to_string()) + })?; + + let req = parse_streaming_body( + &api_key, + req, + &mut content_sha256, + &garage.config.s3_api.s3_region, + "k2v", + )?; + + let bucket_id = resolve_bucket(&garage, &bucket_name, &api_key).await?; + let bucket = garage + .bucket_table + .get(&EmptyKey, &bucket_id) + .await? + .filter(|b| !b.state.is_deleted()) + .ok_or(Error::NoSuchBucket)?; + + let allowed = match endpoint.authorization_type() { + Authorization::Read => api_key.allow_read(&bucket_id), + Authorization::Write => api_key.allow_write(&bucket_id), + Authorization::Owner => api_key.allow_owner(&bucket_id), + _ => unreachable!(), + }; + + if !allowed { + return Err(Error::Forbidden( + "Operation is not allowed for this key.".to_string(), + )); + } + + // Look up what CORS rule might apply to response. + // Requests for methods different than GET, HEAD or POST + // are always preflighted, i.e. the browser should make + // an OPTIONS call before to check it is allowed + let matching_cors_rule = match *req.method() { + Method::GET | Method::HEAD | Method::POST => find_matching_cors_rule(&bucket, &req)?, + _ => None, + }; + + let resp = match endpoint { + Endpoint::DeleteItem { + partition_key, + sort_key, + } => handle_delete_item(garage, req, bucket_id, &partition_key, &sort_key).await, + Endpoint::InsertItem { + partition_key, + sort_key, + } => handle_insert_item(garage, req, bucket_id, &partition_key, &sort_key).await, + Endpoint::ReadItem { + partition_key, + sort_key, + } => handle_read_item(garage, &req, bucket_id, &partition_key, &sort_key).await, + Endpoint::PollItem { + partition_key, + sort_key, + causality_token, + timeout, + } => { + handle_poll_item( + garage, + &req, + bucket_id, + partition_key, + sort_key, + causality_token, + timeout, + ) + .await + } + Endpoint::ReadIndex { + prefix, + start, + end, + limit, + reverse, + } => handle_read_index(garage, bucket_id, prefix, start, end, limit, reverse).await, + Endpoint::InsertBatch {} => handle_insert_batch(garage, bucket_id, req).await, + Endpoint::ReadBatch {} => handle_read_batch(garage, bucket_id, req).await, + Endpoint::DeleteBatch {} => handle_delete_batch(garage, bucket_id, req).await, + Endpoint::Options => unreachable!(), + }; + + // If request was a success and we have a CORS rule that applies to it, + // add the corresponding CORS headers to the response + let mut resp_ok = resp?; + if let Some(rule) = matching_cors_rule { + add_cors_headers(&mut resp_ok, rule) + .ok_or_internal_error("Invalid bucket CORS configuration")?; + } + + Ok(resp_ok) + } +} + +impl ApiEndpoint for K2VApiEndpoint { + fn name(&self) -> &'static str { + self.endpoint.name() + } + + fn add_span_attributes(&self, span: SpanRef<'_>) { + span.set_attribute(KeyValue::new("bucket", self.bucket_name.clone())); + } +} diff --git a/src/api/k2v/batch.rs b/src/api/k2v/batch.rs new file mode 100644 index 00000000..4ecddeb9 --- /dev/null +++ b/src/api/k2v/batch.rs @@ -0,0 +1,368 @@ +use std::sync::Arc; + +use hyper::{Body, Request, Response, StatusCode}; +use serde::{Deserialize, Serialize}; + +use garage_util::data::*; +use garage_util::error::Error as GarageError; + +use garage_table::{EnumerationOrder, TableSchema}; + +use garage_model::garage::Garage; +use garage_model::k2v::causality::*; +use garage_model::k2v::item_table::*; + +use crate::error::*; +use crate::k2v::range::read_range; + +pub async fn handle_insert_batch( + garage: Arc, + bucket_id: Uuid, + req: Request, +) -> Result, Error> { + let body = hyper::body::to_bytes(req.into_body()).await?; + let items: Vec = + serde_json::from_slice(&body).ok_or_bad_request("Invalid JSON")?; + + let mut items2 = vec![]; + for it in items { + let ct = it + .ct + .map(|s| CausalContext::parse(&s)) + .transpose() + .ok_or_bad_request("Invalid causality token")?; + let v = match it.v { + Some(vs) => { + DvvsValue::Value(base64::decode(vs).ok_or_bad_request("Invalid base64 value")?) + } + None => DvvsValue::Deleted, + }; + items2.push((it.pk, it.sk, ct, v)); + } + + garage.k2v.rpc.insert_batch(bucket_id, items2).await?; + + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::empty())?) +} + +pub async fn handle_read_batch( + garage: Arc, + bucket_id: Uuid, + req: Request, +) -> Result, Error> { + let body = hyper::body::to_bytes(req.into_body()).await?; + let queries: Vec = + serde_json::from_slice(&body).ok_or_bad_request("Invalid JSON")?; + + let resp_results = futures::future::join_all( + queries + .into_iter() + .map(|q| handle_read_batch_query(&garage, bucket_id, q)), + ) + .await; + + let mut resps: Vec = vec![]; + for resp in resp_results { + resps.push(resp?); + } + + let resp_json = serde_json::to_string_pretty(&resps).map_err(GarageError::from)?; + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::from(resp_json))?) +} + +async fn handle_read_batch_query( + garage: &Arc, + bucket_id: Uuid, + query: ReadBatchQuery, +) -> Result { + let partition = K2VItemPartition { + bucket_id, + partition_key: query.partition_key.clone(), + }; + + let filter = ItemFilter { + exclude_only_tombstones: !query.tombstones, + conflicts_only: query.conflicts_only, + }; + + let (items, more, next_start) = if query.single_item { + if query.prefix.is_some() || query.end.is_some() || query.limit.is_some() || query.reverse { + return Err(Error::BadRequest("Batch query parameters 'prefix', 'end', 'limit' and 'reverse' must not be set when singleItem is true.".into())); + } + let sk = query + .start + .as_ref() + .ok_or_bad_request("start should be specified if single_item is set")?; + let item = garage + .k2v + .item_table + .get(&partition, sk) + .await? + .filter(|e| K2VItemTable::matches_filter(e, &filter)); + match item { + Some(i) => (vec![ReadBatchResponseItem::from(i)], false, None), + None => (vec![], false, None), + } + } else { + let (items, more, next_start) = read_range( + &garage.k2v.item_table, + &partition, + &query.prefix, + &query.start, + &query.end, + query.limit, + Some(filter), + EnumerationOrder::from_reverse(query.reverse), + ) + .await?; + + let items = items + .into_iter() + .map(ReadBatchResponseItem::from) + .collect::>(); + + (items, more, next_start) + }; + + Ok(ReadBatchResponse { + partition_key: query.partition_key, + prefix: query.prefix, + start: query.start, + end: query.end, + limit: query.limit, + reverse: query.reverse, + single_item: query.single_item, + conflicts_only: query.conflicts_only, + tombstones: query.tombstones, + items, + more, + next_start, + }) +} + +pub async fn handle_delete_batch( + garage: Arc, + bucket_id: Uuid, + req: Request, +) -> Result, Error> { + let body = hyper::body::to_bytes(req.into_body()).await?; + let queries: Vec = + serde_json::from_slice(&body).ok_or_bad_request("Invalid JSON")?; + + let resp_results = futures::future::join_all( + queries + .into_iter() + .map(|q| handle_delete_batch_query(&garage, bucket_id, q)), + ) + .await; + + let mut resps: Vec = vec![]; + for resp in resp_results { + resps.push(resp?); + } + + let resp_json = serde_json::to_string_pretty(&resps).map_err(GarageError::from)?; + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::from(resp_json))?) +} + +async fn handle_delete_batch_query( + garage: &Arc, + bucket_id: Uuid, + query: DeleteBatchQuery, +) -> Result { + let partition = K2VItemPartition { + bucket_id, + partition_key: query.partition_key.clone(), + }; + + let filter = ItemFilter { + exclude_only_tombstones: true, + conflicts_only: false, + }; + + let deleted_items = if query.single_item { + if query.prefix.is_some() || query.end.is_some() { + return Err(Error::BadRequest("Batch query parameters 'prefix' and 'end' must not be set when singleItem is true.".into())); + } + let sk = query + .start + .as_ref() + .ok_or_bad_request("start should be specified if single_item is set")?; + let item = garage + .k2v + .item_table + .get(&partition, sk) + .await? + .filter(|e| K2VItemTable::matches_filter(e, &filter)); + match item { + Some(i) => { + let cc = i.causal_context(); + garage + .k2v + .rpc + .insert( + bucket_id, + i.partition.partition_key, + i.sort_key, + Some(cc), + DvvsValue::Deleted, + ) + .await?; + 1 + } + None => 0, + } + } else { + let (items, more, _next_start) = read_range( + &garage.k2v.item_table, + &partition, + &query.prefix, + &query.start, + &query.end, + None, + Some(filter), + EnumerationOrder::Forward, + ) + .await?; + assert!(!more); + + // TODO delete items + let items = items + .into_iter() + .map(|i| { + let cc = i.causal_context(); + ( + i.partition.partition_key, + i.sort_key, + Some(cc), + DvvsValue::Deleted, + ) + }) + .collect::>(); + let n = items.len(); + + garage.k2v.rpc.insert_batch(bucket_id, items).await?; + + n + }; + + Ok(DeleteBatchResponse { + partition_key: query.partition_key, + prefix: query.prefix, + start: query.start, + end: query.end, + single_item: query.single_item, + deleted_items, + }) +} + +#[derive(Deserialize)] +struct InsertBatchItem { + pk: String, + sk: String, + ct: Option, + v: Option, +} + +#[derive(Deserialize)] +struct ReadBatchQuery { + #[serde(rename = "partitionKey")] + partition_key: String, + #[serde(default)] + prefix: Option, + #[serde(default)] + start: Option, + #[serde(default)] + end: Option, + #[serde(default)] + limit: Option, + #[serde(default)] + reverse: bool, + #[serde(default, rename = "singleItem")] + single_item: bool, + #[serde(default, rename = "conflictsOnly")] + conflicts_only: bool, + #[serde(default)] + tombstones: bool, +} + +#[derive(Serialize)] +struct ReadBatchResponse { + #[serde(rename = "partitionKey")] + partition_key: String, + prefix: Option, + start: Option, + end: Option, + limit: Option, + reverse: bool, + #[serde(rename = "singleItem")] + single_item: bool, + #[serde(rename = "conflictsOnly")] + conflicts_only: bool, + tombstones: bool, + + items: Vec, + more: bool, + #[serde(rename = "nextStart")] + next_start: Option, +} + +#[derive(Serialize)] +struct ReadBatchResponseItem { + sk: String, + ct: String, + v: Vec>, +} + +impl ReadBatchResponseItem { + fn from(i: K2VItem) -> Self { + let ct = i.causal_context().serialize(); + let v = i + .values() + .iter() + .map(|v| match v { + DvvsValue::Value(x) => Some(base64::encode(x)), + DvvsValue::Deleted => None, + }) + .collect::>(); + Self { + sk: i.sort_key, + ct, + v, + } + } +} + +#[derive(Deserialize)] +struct DeleteBatchQuery { + #[serde(rename = "partitionKey")] + partition_key: String, + #[serde(default)] + prefix: Option, + #[serde(default)] + start: Option, + #[serde(default)] + end: Option, + #[serde(default, rename = "singleItem")] + single_item: bool, +} + +#[derive(Serialize)] +struct DeleteBatchResponse { + #[serde(rename = "partitionKey")] + partition_key: String, + prefix: Option, + start: Option, + end: Option, + #[serde(rename = "singleItem")] + single_item: bool, + + #[serde(rename = "deletedItems")] + deleted_items: usize, +} diff --git a/src/api/k2v/index.rs b/src/api/k2v/index.rs new file mode 100644 index 00000000..896dbcf0 --- /dev/null +++ b/src/api/k2v/index.rs @@ -0,0 +1,100 @@ +use std::sync::Arc; + +use hyper::{Body, Response, StatusCode}; +use serde::Serialize; + +use garage_util::data::*; +use garage_util::error::Error as GarageError; + +use garage_rpc::ring::Ring; +use garage_table::util::*; + +use garage_model::garage::Garage; +use garage_model::k2v::counter_table::{BYTES, CONFLICTS, ENTRIES, VALUES}; + +use crate::error::*; +use crate::k2v::range::read_range; + +pub async fn handle_read_index( + garage: Arc, + bucket_id: Uuid, + prefix: Option, + start: Option, + end: Option, + limit: Option, + reverse: Option, +) -> Result, Error> { + let reverse = reverse.unwrap_or(false); + + let ring: Arc = garage.system.ring.borrow().clone(); + + let (partition_keys, more, next_start) = read_range( + &garage.k2v.counter_table.table, + &bucket_id, + &prefix, + &start, + &end, + limit, + Some((DeletedFilter::NotDeleted, ring.layout.node_id_vec.clone())), + EnumerationOrder::from_reverse(reverse), + ) + .await?; + + let s_entries = ENTRIES.to_string(); + let s_conflicts = CONFLICTS.to_string(); + let s_values = VALUES.to_string(); + let s_bytes = BYTES.to_string(); + + let resp = ReadIndexResponse { + prefix, + start, + end, + limit, + reverse, + partition_keys: partition_keys + .into_iter() + .map(|part| { + let vals = part.filtered_values(&ring); + ReadIndexResponseEntry { + pk: part.sk, + entries: *vals.get(&s_entries).unwrap_or(&0), + conflicts: *vals.get(&s_conflicts).unwrap_or(&0), + values: *vals.get(&s_values).unwrap_or(&0), + bytes: *vals.get(&s_bytes).unwrap_or(&0), + } + }) + .collect::>(), + more, + next_start, + }; + + let resp_json = serde_json::to_string_pretty(&resp).map_err(GarageError::from)?; + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::from(resp_json))?) +} + +#[derive(Serialize)] +struct ReadIndexResponse { + prefix: Option, + start: Option, + end: Option, + limit: Option, + reverse: bool, + + #[serde(rename = "partitionKeys")] + partition_keys: Vec, + + more: bool, + #[serde(rename = "nextStart")] + next_start: Option, +} + +#[derive(Serialize)] +struct ReadIndexResponseEntry { + pk: String, + entries: i64, + conflicts: i64, + values: i64, + bytes: i64, +} diff --git a/src/api/k2v/item.rs b/src/api/k2v/item.rs new file mode 100644 index 00000000..1860863e --- /dev/null +++ b/src/api/k2v/item.rs @@ -0,0 +1,230 @@ +use std::sync::Arc; + +use http::header; + +use hyper::{Body, Request, Response, StatusCode}; + +use garage_util::data::*; + +use garage_model::garage::Garage; +use garage_model::k2v::causality::*; +use garage_model::k2v::item_table::*; + +use crate::error::*; + +pub const X_GARAGE_CAUSALITY_TOKEN: &str = "X-Garage-Causality-Token"; + +pub enum ReturnFormat { + Json, + Binary, + Either, +} + +impl ReturnFormat { + pub fn from(req: &Request) -> Result { + let accept = match req.headers().get(header::ACCEPT) { + Some(a) => a.to_str()?, + None => return Ok(Self::Json), + }; + + let accept = accept.split(',').map(|s| s.trim()).collect::>(); + let accept_json = accept.contains(&"application/json") || accept.contains(&"*/*"); + let accept_binary = accept.contains(&"application/octet-stream") || accept.contains(&"*/*"); + + match (accept_json, accept_binary) { + (true, true) => Ok(Self::Either), + (true, false) => Ok(Self::Json), + (false, true) => Ok(Self::Binary), + (false, false) => Err(Error::NotAcceptable("Invalid Accept: header value, must contain either application/json or application/octet-stream (or both)".into())), + } + } + + pub fn make_response(&self, item: &K2VItem) -> Result, Error> { + let vals = item.values(); + + if vals.is_empty() { + return Err(Error::NoSuchKey); + } + + let ct = item.causal_context().serialize(); + match self { + Self::Binary if vals.len() > 1 => Ok(Response::builder() + .header(X_GARAGE_CAUSALITY_TOKEN, ct) + .status(StatusCode::CONFLICT) + .body(Body::empty())?), + Self::Binary => { + assert!(vals.len() == 1); + Self::make_binary_response(ct, vals[0]) + } + Self::Either if vals.len() == 1 => Self::make_binary_response(ct, vals[0]), + _ => Self::make_json_response(ct, &vals[..]), + } + } + + fn make_binary_response(ct: String, v: &DvvsValue) -> Result, Error> { + match v { + DvvsValue::Deleted => Ok(Response::builder() + .header(X_GARAGE_CAUSALITY_TOKEN, ct) + .header(header::CONTENT_TYPE, "application/octet-stream") + .status(StatusCode::NO_CONTENT) + .body(Body::empty())?), + DvvsValue::Value(v) => Ok(Response::builder() + .header(X_GARAGE_CAUSALITY_TOKEN, ct) + .header(header::CONTENT_TYPE, "application/octet-stream") + .status(StatusCode::OK) + .body(Body::from(v.to_vec()))?), + } + } + + fn make_json_response(ct: String, v: &[&DvvsValue]) -> Result, Error> { + let items = v + .iter() + .map(|v| match v { + DvvsValue::Deleted => serde_json::Value::Null, + DvvsValue::Value(v) => serde_json::Value::String(base64::encode(v)), + }) + .collect::>(); + let json_body = + serde_json::to_string_pretty(&items).ok_or_internal_error("JSON encoding error")?; + Ok(Response::builder() + .header(X_GARAGE_CAUSALITY_TOKEN, ct) + .header(header::CONTENT_TYPE, "application/json") + .status(StatusCode::OK) + .body(Body::from(json_body))?) + } +} + +/// Handle ReadItem request +#[allow(clippy::ptr_arg)] +pub async fn handle_read_item( + garage: Arc, + req: &Request, + bucket_id: Uuid, + partition_key: &str, + sort_key: &String, +) -> Result, Error> { + let format = ReturnFormat::from(req)?; + + let item = garage + .k2v + .item_table + .get( + &K2VItemPartition { + bucket_id, + partition_key: partition_key.to_string(), + }, + sort_key, + ) + .await? + .ok_or(Error::NoSuchKey)?; + + format.make_response(&item) +} + +pub async fn handle_insert_item( + garage: Arc, + req: Request, + bucket_id: Uuid, + partition_key: &str, + sort_key: &str, +) -> Result, Error> { + let causal_context = req + .headers() + .get(X_GARAGE_CAUSALITY_TOKEN) + .map(|s| s.to_str()) + .transpose()? + .map(CausalContext::parse) + .transpose() + .ok_or_bad_request("Invalid causality token")?; + + let body = hyper::body::to_bytes(req.into_body()).await?; + let value = DvvsValue::Value(body.to_vec()); + + garage + .k2v + .rpc + .insert( + bucket_id, + partition_key.to_string(), + sort_key.to_string(), + causal_context, + value, + ) + .await?; + + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::empty())?) +} + +pub async fn handle_delete_item( + garage: Arc, + req: Request, + bucket_id: Uuid, + partition_key: &str, + sort_key: &str, +) -> Result, Error> { + let causal_context = req + .headers() + .get(X_GARAGE_CAUSALITY_TOKEN) + .map(|s| s.to_str()) + .transpose()? + .map(CausalContext::parse) + .transpose() + .ok_or_bad_request("Invalid causality token")?; + + let value = DvvsValue::Deleted; + + garage + .k2v + .rpc + .insert( + bucket_id, + partition_key.to_string(), + sort_key.to_string(), + causal_context, + value, + ) + .await?; + + Ok(Response::builder() + .status(StatusCode::NO_CONTENT) + .body(Body::empty())?) +} + +/// Handle ReadItem request +#[allow(clippy::ptr_arg)] +pub async fn handle_poll_item( + garage: Arc, + req: &Request, + bucket_id: Uuid, + partition_key: String, + sort_key: String, + causality_token: String, + timeout_secs: Option, +) -> Result, Error> { + let format = ReturnFormat::from(req)?; + + let causal_context = + CausalContext::parse(&causality_token).ok_or_bad_request("Invalid causality token")?; + + let item = garage + .k2v + .rpc + .poll( + bucket_id, + partition_key, + sort_key, + causal_context, + timeout_secs.unwrap_or(300) * 1000, + ) + .await?; + + if let Some(item) = item { + format.make_response(&item) + } else { + Ok(Response::builder() + .status(StatusCode::NOT_MODIFIED) + .body(Body::empty())?) + } +} diff --git a/src/api/k2v/mod.rs b/src/api/k2v/mod.rs new file mode 100644 index 00000000..ee210ad5 --- /dev/null +++ b/src/api/k2v/mod.rs @@ -0,0 +1,8 @@ +pub mod api_server; +mod router; + +mod batch; +mod index; +mod item; + +mod range; diff --git a/src/api/k2v/range.rs b/src/api/k2v/range.rs new file mode 100644 index 00000000..cd019723 --- /dev/null +++ b/src/api/k2v/range.rs @@ -0,0 +1,96 @@ +//! Utility module for retrieving ranges of items in Garage tables +//! Implements parameters (prefix, start, end, limit) as specified +//! for endpoints ReadIndex, ReadBatch and DeleteBatch + +use std::sync::Arc; + +use garage_table::replication::TableShardedReplication; +use garage_table::*; + +use crate::error::*; +use crate::helpers::key_after_prefix; + +/// Read range in a Garage table. +/// Returns (entries, more?, nextStart) +#[allow(clippy::too_many_arguments)] +pub(crate) async fn read_range( + table: &Arc>, + partition_key: &F::P, + prefix: &Option, + start: &Option, + end: &Option, + limit: Option, + filter: Option, + enumeration_order: EnumerationOrder, +) -> Result<(Vec, bool, Option), Error> +where + F: TableSchema + 'static, +{ + let (mut start, mut start_ignore) = match (prefix, start) { + (None, None) => (None, false), + (None, Some(s)) => (Some(s.clone()), false), + (Some(p), Some(s)) => { + if !s.starts_with(p) { + return Err(Error::BadRequest(format!( + "Start key '{}' does not start with prefix '{}'", + s, p + ))); + } + (Some(s.clone()), false) + } + (Some(p), None) if enumeration_order == EnumerationOrder::Reverse => { + let start = key_after_prefix(p) + .ok_or_internal_error("Sorry, can't list this prefix in reverse order")?; + (Some(start), true) + } + (Some(p), None) => (Some(p.clone()), false), + }; + + let mut entries = vec![]; + loop { + let n_get = std::cmp::min( + 1000, + limit.map(|x| x as usize).unwrap_or(usize::MAX - 10) - entries.len() + 2, + ); + let get_ret = table + .get_range( + partition_key, + start.clone(), + filter.clone(), + n_get, + enumeration_order, + ) + .await?; + + let get_ret_len = get_ret.len(); + + for entry in get_ret { + if start_ignore && Some(entry.sort_key()) == start.as_ref() { + continue; + } + if let Some(p) = prefix { + if !entry.sort_key().starts_with(p) { + return Ok((entries, false, None)); + } + } + if let Some(e) = end { + if entry.sort_key() == e { + return Ok((entries, false, None)); + } + } + if let Some(l) = limit { + if entries.len() >= l as usize { + return Ok((entries, true, Some(entry.sort_key().clone()))); + } + } + entries.push(entry); + } + + if get_ret_len < n_get { + return Ok((entries, false, None)); + } + + start = Some(entries.last().unwrap().sort_key().clone()); + start_ignore = true; + } +} diff --git a/src/api/k2v/router.rs b/src/api/k2v/router.rs new file mode 100644 index 00000000..f948ffce --- /dev/null +++ b/src/api/k2v/router.rs @@ -0,0 +1,252 @@ +use crate::error::*; + +use std::borrow::Cow; + +use hyper::{Method, Request}; + +use crate::helpers::Authorization; +use crate::router_macros::{generateQueryParameters, router_match}; + +router_match! {@func + + +/// List of all K2V API endpoints. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Endpoint { + DeleteBatch { + }, + DeleteItem { + partition_key: String, + sort_key: String, + }, + InsertBatch { + }, + InsertItem { + partition_key: String, + sort_key: String, + }, + Options, + PollItem { + partition_key: String, + sort_key: String, + causality_token: String, + timeout: Option, + }, + ReadBatch { + }, + ReadIndex { + prefix: Option, + start: Option, + end: Option, + limit: Option, + reverse: Option, + }, + ReadItem { + partition_key: String, + sort_key: String, + }, +}} + +impl Endpoint { + /// Determine which S3 endpoint a request is for using the request, and a bucket which was + /// possibly extracted from the Host header. + /// Returns Self plus bucket name, if endpoint is not Endpoint::ListBuckets + pub fn from_request(req: &Request) -> Result<(Self, String), Error> { + let uri = req.uri(); + let path = uri.path().trim_start_matches('/'); + let query = uri.query(); + + let (bucket, partition_key) = path + .split_once('/') + .map(|(b, p)| (b.to_owned(), p.trim_start_matches('/'))) + .unwrap_or((path.to_owned(), "")); + + if bucket.is_empty() { + return Err(Error::BadRequest("Missing bucket name".to_owned())); + } + + if *req.method() == Method::OPTIONS { + return Ok((Self::Options, bucket)); + } + + let partition_key = percent_encoding::percent_decode_str(partition_key) + .decode_utf8()? + .into_owned(); + + let mut query = QueryParameters::from_query(query.unwrap_or_default())?; + + let method_search = Method::from_bytes(b"SEARCH").unwrap(); + let res = match *req.method() { + Method::GET => Self::from_get(partition_key, &mut query)?, + //&Method::HEAD => Self::from_head(partition_key, &mut query)?, + Method::POST => Self::from_post(partition_key, &mut query)?, + Method::PUT => Self::from_put(partition_key, &mut query)?, + Method::DELETE => Self::from_delete(partition_key, &mut query)?, + _ if req.method() == method_search => Self::from_search(partition_key, &mut query)?, + _ => return Err(Error::BadRequest("Unknown method".to_owned())), + }; + + if let Some(message) = query.nonempty_message() { + debug!("Unused query parameter: {}", message) + } + Ok((res, bucket)) + } + + /// Determine which endpoint a request is for, knowing it is a GET. + fn from_get(partition_key: String, query: &mut QueryParameters<'_>) -> Result { + router_match! { + @gen_parser + (query.keyword.take().unwrap_or_default().as_ref(), partition_key, query, None), + key: [ + EMPTY if causality_token => PollItem (query::sort_key, query::causality_token, opt_parse::timeout), + EMPTY => ReadItem (query::sort_key), + ], + no_key: [ + EMPTY => ReadIndex (query_opt::prefix, query_opt::start, query_opt::end, opt_parse::limit, opt_parse::reverse), + ] + } + } + + /// Determine which endpoint a request is for, knowing it is a SEARCH. + fn from_search(partition_key: String, query: &mut QueryParameters<'_>) -> Result { + router_match! { + @gen_parser + (query.keyword.take().unwrap_or_default().as_ref(), partition_key, query, None), + key: [ + ], + no_key: [ + EMPTY => ReadBatch, + ] + } + } + + /* + /// Determine which endpoint a request is for, knowing it is a HEAD. + fn from_head(partition_key: String, query: &mut QueryParameters<'_>) -> Result { + router_match! { + @gen_parser + (query.keyword.take().unwrap_or_default().as_ref(), partition_key, query, None), + key: [ + EMPTY => HeadObject(opt_parse::part_number, query_opt::version_id), + ], + no_key: [ + EMPTY => HeadBucket, + ] + } + } + */ + + /// Determine which endpoint a request is for, knowing it is a POST. + fn from_post(partition_key: String, query: &mut QueryParameters<'_>) -> Result { + router_match! { + @gen_parser + (query.keyword.take().unwrap_or_default().as_ref(), partition_key, query, None), + key: [ + ], + no_key: [ + EMPTY => InsertBatch, + DELETE => DeleteBatch, + SEARCH => ReadBatch, + ] + } + } + + /// Determine which endpoint a request is for, knowing it is a PUT. + fn from_put(partition_key: String, query: &mut QueryParameters<'_>) -> Result { + router_match! { + @gen_parser + (query.keyword.take().unwrap_or_default().as_ref(), partition_key, query, None), + key: [ + EMPTY => InsertItem (query::sort_key), + + ], + no_key: [ + ] + } + } + + /// Determine which endpoint a request is for, knowing it is a DELETE. + fn from_delete(partition_key: String, query: &mut QueryParameters<'_>) -> Result { + router_match! { + @gen_parser + (query.keyword.take().unwrap_or_default().as_ref(), partition_key, query, None), + key: [ + EMPTY => DeleteItem (query::sort_key), + ], + no_key: [ + ] + } + } + + /// Get the partition key the request target. Returns None for requests which don't use a partition key. + #[allow(dead_code)] + pub fn get_partition_key(&self) -> Option<&str> { + router_match! { + @extract + self, + partition_key, + [ + DeleteItem, + InsertItem, + PollItem, + ReadItem, + ] + } + } + + /// Get the sort key the request target. Returns None for requests which don't use a sort key. + #[allow(dead_code)] + pub fn get_sort_key(&self) -> Option<&str> { + router_match! { + @extract + self, + sort_key, + [ + DeleteItem, + InsertItem, + PollItem, + ReadItem, + ] + } + } + + /// Get the kind of authorization which is required to perform the operation. + pub fn authorization_type(&self) -> Authorization { + let readonly = router_match! { + @match + self, + [ + PollItem, + ReadBatch, + ReadIndex, + ReadItem, + ] + }; + if readonly { + Authorization::Read + } else { + Authorization::Write + } + } +} + +// parameter name => struct field +generateQueryParameters! { + "prefix" => prefix, + "start" => start, + "causality_token" => causality_token, + "end" => end, + "limit" => limit, + "reverse" => reverse, + "sort_key" => sort_key, + "timeout" => timeout +} + +mod keywords { + //! This module contain all query parameters with no associated value + //! used to differentiate endpoints. + pub const EMPTY: &str = ""; + + pub const DELETE: &str = "delete"; + pub const SEARCH: &str = "search"; +} diff --git a/src/api/lib.rs b/src/api/lib.rs index de60ec53..0078f7b5 100644 --- a/src/api/lib.rs +++ b/src/api/lib.rs @@ -6,22 +6,12 @@ pub mod error; pub use error::Error; mod encoding; - -mod api_server; -pub use api_server::run_api_server; - +mod generic_server; +pub mod helpers; +mod router_macros; /// This mode is public only to help testing. Don't expect stability here pub mod signature; -pub mod helpers; -mod s3_bucket; -mod s3_copy; -pub mod s3_cors; -mod s3_delete; -pub mod s3_get; -mod s3_list; -mod s3_post_object; -mod s3_put; -mod s3_router; -mod s3_website; -mod s3_xml; +#[cfg(feature = "k2v")] +pub mod k2v; +pub mod s3; diff --git a/src/api/router_macros.rs b/src/api/router_macros.rs new file mode 100644 index 00000000..8471407c --- /dev/null +++ b/src/api/router_macros.rs @@ -0,0 +1,190 @@ +/// This macro is used to generate very repetitive match {} blocks in this module +/// It is _not_ made to be used anywhere else +macro_rules! router_match { + (@match $enum:expr , [ $($endpoint:ident,)* ]) => {{ + // usage: router_match {@match my_enum, [ VariantWithField1, VariantWithField2 ..] } + // returns true if the variant was one of the listed variants, false otherwise. + use Endpoint::*; + match $enum { + $( + $endpoint { .. } => true, + )* + _ => false + } + }}; + (@extract $enum:expr , $param:ident, [ $($endpoint:ident,)* ]) => {{ + // usage: router_match {@extract my_enum, field_name, [ VariantWithField1, VariantWithField2 ..] } + // returns Some(field_value), or None if the variant was not one of the listed variants. + use Endpoint::*; + match $enum { + $( + $endpoint {$param, ..} => Some($param), + )* + _ => None + } + }}; + (@gen_parser ($keyword:expr, $key:ident, $query:expr, $header:expr), + key: [$($kw_k:ident $(if $required_k:ident)? $(header $header_k:expr)? => $api_k:ident $(($($conv_k:ident :: $param_k:ident),*))?,)*], + no_key: [$($kw_nk:ident $(if $required_nk:ident)? $(if_header $header_nk:expr)? => $api_nk:ident $(($($conv_nk:ident :: $param_nk:ident),*))?,)*]) => {{ + // usage: router_match {@gen_parser (keyword, key, query, header), + // key: [ + // SOME_KEYWORD => VariantWithKey, + // ... + // ], + // no_key: [ + // SOME_KEYWORD => VariantWithoutKey, + // ... + // ] + // } + // See in from_{method} for more detailed usage. + use Endpoint::*; + use keywords::*; + match ($keyword, !$key.is_empty()){ + $( + ($kw_k, true) if true $(&& $query.$required_k.is_some())? $(&& $header.contains_key($header_k))? => Ok($api_k { + $key, + $($( + $param_k: router_match!(@@parse_param $query, $conv_k, $param_k), + )*)? + }), + )* + $( + ($kw_nk, false) $(if $query.$required_nk.is_some())? $(if $header.contains($header_nk))? => Ok($api_nk { + $($( + $param_nk: router_match!(@@parse_param $query, $conv_nk, $param_nk), + )*)? + }), + )* + (kw, _) => Err(Error::BadRequest(format!("Invalid endpoint: {}", kw))) + } + }}; + + (@@parse_param $query:expr, query_opt, $param:ident) => {{ + // extract optional query parameter + $query.$param.take().map(|param| param.into_owned()) + }}; + (@@parse_param $query:expr, query, $param:ident) => {{ + // extract mendatory query parameter + $query.$param.take().ok_or_bad_request("Missing argument for endpoint")?.into_owned() + }}; + (@@parse_param $query:expr, opt_parse, $param:ident) => {{ + // extract and parse optional query parameter + // missing parameter is file, however parse error is reported as an error + $query.$param + .take() + .map(|param| param.parse()) + .transpose() + .map_err(|_| Error::BadRequest("Failed to parse query parameter".to_owned()))? + }}; + (@@parse_param $query:expr, parse, $param:ident) => {{ + // extract and parse mandatory query parameter + // both missing and un-parseable parameters are reported as errors + $query.$param.take().ok_or_bad_request("Missing argument for endpoint")? + .parse() + .map_err(|_| Error::BadRequest("Failed to parse query parameter".to_owned()))? + }}; + (@func + $(#[$doc:meta])* + pub enum Endpoint { + $( + $(#[$outer:meta])* + $variant:ident $({ + $($name:ident: $ty:ty,)* + })?, + )* + }) => { + $(#[$doc])* + pub enum Endpoint { + $( + $(#[$outer])* + $variant $({ + $($name: $ty, )* + })?, + )* + } + impl Endpoint { + pub fn name(&self) -> &'static str { + match self { + $(Endpoint::$variant $({ $($name: _,)* .. })? => stringify!($variant),)* + } + } + } + }; + (@if ($($cond:tt)+) then ($($then:tt)*) else ($($else:tt)*)) => { + $($then)* + }; + (@if () then ($($then:tt)*) else ($($else:tt)*)) => { + $($else)* + }; +} + +/// This macro is used to generate part of the code in this module. It must be called only one, and +/// is useless outside of this module. +macro_rules! generateQueryParameters { + ( $($rest:expr => $name:ident),* ) => { + /// Struct containing all query parameters used in endpoints. Think of it as an HashMap, + /// but with keys statically known. + #[derive(Debug, Default)] + struct QueryParameters<'a> { + keyword: Option>, + $( + $name: Option>, + )* + } + + impl<'a> QueryParameters<'a> { + /// Build this struct from the query part of an URI. + fn from_query(query: &'a str) -> Result { + let mut res: Self = Default::default(); + for (k, v) in url::form_urlencoded::parse(query.as_bytes()) { + let repeated = match k.as_ref() { + $( + $rest => if !v.is_empty() { + res.$name.replace(v).is_some() + } else { + false + }, + )* + _ => { + if k.starts_with("response-") || k.starts_with("X-Amz-") { + false + } else if v.as_ref().is_empty() { + if res.keyword.replace(k).is_some() { + return Err(Error::BadRequest("Multiple keywords".to_owned())); + } + continue; + } else { + debug!("Received an unknown query parameter: '{}'", k); + false + } + } + }; + if repeated { + return Err(Error::BadRequest(format!( + "Query parameter repeated: '{}'", + k + ))); + } + } + Ok(res) + } + + /// Get an error message in case not all parameters where used when extracting them to + /// build an Enpoint variant + fn nonempty_message(&self) -> Option<&str> { + if self.keyword.is_some() { + Some("Keyword not used") + } $( + else if self.$name.is_some() { + Some(concat!("'", $rest, "'")) + } + )* else { + None + } + } + } + } +} + +pub(crate) use generateQueryParameters; +pub(crate) use router_match; diff --git a/src/api/s3/api_server.rs b/src/api/s3/api_server.rs new file mode 100644 index 00000000..78a69d53 --- /dev/null +++ b/src/api/s3/api_server.rs @@ -0,0 +1,401 @@ +use std::sync::Arc; + +use async_trait::async_trait; + +use futures::future::Future; +use hyper::header; +use hyper::{Body, Method, Request, Response}; + +use opentelemetry::{trace::SpanRef, KeyValue}; + +use garage_table::util::*; +use garage_util::error::Error as GarageError; + +use garage_model::garage::Garage; +use garage_model::key_table::Key; + +use crate::error::*; +use crate::generic_server::*; + +use crate::signature::payload::check_payload_signature; +use crate::signature::streaming::*; + +use crate::helpers::*; +use crate::s3::bucket::*; +use crate::s3::copy::*; +use crate::s3::cors::*; +use crate::s3::delete::*; +use crate::s3::get::*; +use crate::s3::list::*; +use crate::s3::post_object::handle_post_object; +use crate::s3::put::*; +use crate::s3::router::Endpoint; +use crate::s3::website::*; + +pub struct S3ApiServer { + garage: Arc, +} + +pub(crate) struct S3ApiEndpoint { + bucket_name: Option, + endpoint: Endpoint, +} + +impl S3ApiServer { + pub async fn run( + garage: Arc, + shutdown_signal: impl Future, + ) -> Result<(), GarageError> { + let addr = garage.config.s3_api.api_bind_addr; + + ApiServer::new( + garage.config.s3_api.s3_region.clone(), + S3ApiServer { garage }, + ) + .run_server(addr, shutdown_signal) + .await + } + + async fn handle_request_without_bucket( + &self, + _req: Request, + api_key: Key, + endpoint: Endpoint, + ) -> Result, Error> { + match endpoint { + Endpoint::ListBuckets => handle_list_buckets(&self.garage, &api_key).await, + endpoint => Err(Error::NotImplemented(endpoint.name().to_owned())), + } + } +} + +#[async_trait] +impl ApiHandler for S3ApiServer { + const API_NAME: &'static str = "s3"; + const API_NAME_DISPLAY: &'static str = "S3"; + + type Endpoint = S3ApiEndpoint; + + fn parse_endpoint(&self, req: &Request) -> Result { + let authority = req + .headers() + .get(header::HOST) + .ok_or_bad_request("Host header required")? + .to_str()?; + + let host = authority_to_host(authority)?; + + let bucket_name = self + .garage + .config + .s3_api + .root_domain + .as_ref() + .and_then(|root_domain| host_to_bucket(&host, root_domain)); + + let (endpoint, bucket_name) = + Endpoint::from_request(req, bucket_name.map(ToOwned::to_owned))?; + + Ok(S3ApiEndpoint { + bucket_name, + endpoint, + }) + } + + async fn handle( + &self, + req: Request, + endpoint: S3ApiEndpoint, + ) -> Result, Error> { + let S3ApiEndpoint { + bucket_name, + endpoint, + } = endpoint; + let garage = self.garage.clone(); + + // Some endpoints are processed early, before we even check for an API key + if let Endpoint::PostObject = endpoint { + return handle_post_object(garage, req, bucket_name.unwrap()).await; + } + if let Endpoint::Options = endpoint { + return handle_options_s3api(garage, &req, bucket_name).await; + } + + let (api_key, mut content_sha256) = check_payload_signature(&garage, "s3", &req).await?; + let api_key = api_key.ok_or_else(|| { + Error::Forbidden("Garage does not support anonymous access yet".to_string()) + })?; + + let req = parse_streaming_body( + &api_key, + req, + &mut content_sha256, + &garage.config.s3_api.s3_region, + "s3", + )?; + + let bucket_name = match bucket_name { + None => { + return self + .handle_request_without_bucket(req, api_key, endpoint) + .await + } + Some(bucket) => bucket.to_string(), + }; + + // Special code path for CreateBucket API endpoint + if let Endpoint::CreateBucket {} = endpoint { + return handle_create_bucket(&garage, req, content_sha256, api_key, bucket_name).await; + } + + let bucket_id = resolve_bucket(&garage, &bucket_name, &api_key).await?; + let bucket = garage + .bucket_table + .get(&EmptyKey, &bucket_id) + .await? + .filter(|b| !b.state.is_deleted()) + .ok_or(Error::NoSuchBucket)?; + + let allowed = match endpoint.authorization_type() { + Authorization::Read => api_key.allow_read(&bucket_id), + Authorization::Write => api_key.allow_write(&bucket_id), + Authorization::Owner => api_key.allow_owner(&bucket_id), + _ => unreachable!(), + }; + + if !allowed { + return Err(Error::Forbidden( + "Operation is not allowed for this key.".to_string(), + )); + } + + // Look up what CORS rule might apply to response. + // Requests for methods different than GET, HEAD or POST + // are always preflighted, i.e. the browser should make + // an OPTIONS call before to check it is allowed + let matching_cors_rule = match *req.method() { + Method::GET | Method::HEAD | Method::POST => find_matching_cors_rule(&bucket, &req)?, + _ => None, + }; + + let resp = match endpoint { + Endpoint::HeadObject { + key, part_number, .. + } => handle_head(garage, &req, bucket_id, &key, part_number).await, + Endpoint::GetObject { + key, part_number, .. + } => handle_get(garage, &req, bucket_id, &key, part_number).await, + Endpoint::UploadPart { + key, + part_number, + upload_id, + } => { + handle_put_part( + garage, + req, + bucket_id, + &key, + part_number, + &upload_id, + content_sha256, + ) + .await + } + Endpoint::CopyObject { key } => { + handle_copy(garage, &api_key, &req, bucket_id, &key).await + } + Endpoint::UploadPartCopy { + key, + part_number, + upload_id, + } => { + handle_upload_part_copy( + garage, + &api_key, + &req, + bucket_id, + &key, + part_number, + &upload_id, + ) + .await + } + Endpoint::PutObject { key } => { + handle_put(garage, req, bucket_id, &key, content_sha256).await + } + Endpoint::AbortMultipartUpload { key, upload_id } => { + handle_abort_multipart_upload(garage, bucket_id, &key, &upload_id).await + } + Endpoint::DeleteObject { key, .. } => handle_delete(garage, bucket_id, &key).await, + Endpoint::CreateMultipartUpload { key } => { + handle_create_multipart_upload(garage, &req, &bucket_name, bucket_id, &key).await + } + Endpoint::CompleteMultipartUpload { key, upload_id } => { + handle_complete_multipart_upload( + garage, + req, + &bucket_name, + bucket_id, + &key, + &upload_id, + content_sha256, + ) + .await + } + Endpoint::CreateBucket {} => unreachable!(), + Endpoint::HeadBucket {} => { + let empty_body: Body = Body::from(vec![]); + let response = Response::builder().body(empty_body).unwrap(); + Ok(response) + } + Endpoint::DeleteBucket {} => { + handle_delete_bucket(&garage, bucket_id, bucket_name, api_key).await + } + Endpoint::GetBucketLocation {} => handle_get_bucket_location(garage), + Endpoint::GetBucketVersioning {} => handle_get_bucket_versioning(), + Endpoint::ListObjects { + delimiter, + encoding_type, + marker, + max_keys, + prefix, + } => { + handle_list( + garage, + &ListObjectsQuery { + common: ListQueryCommon { + bucket_name, + bucket_id, + delimiter: delimiter.map(|d| d.to_string()), + page_size: max_keys.map(|p| p.clamp(1, 1000)).unwrap_or(1000), + prefix: prefix.unwrap_or_default(), + urlencode_resp: encoding_type.map(|e| e == "url").unwrap_or(false), + }, + is_v2: false, + marker, + continuation_token: None, + start_after: None, + }, + ) + .await + } + Endpoint::ListObjectsV2 { + delimiter, + encoding_type, + max_keys, + prefix, + continuation_token, + start_after, + list_type, + .. + } => { + if list_type == "2" { + handle_list( + garage, + &ListObjectsQuery { + common: ListQueryCommon { + bucket_name, + bucket_id, + delimiter: delimiter.map(|d| d.to_string()), + page_size: max_keys.map(|p| p.clamp(1, 1000)).unwrap_or(1000), + urlencode_resp: encoding_type.map(|e| e == "url").unwrap_or(false), + prefix: prefix.unwrap_or_default(), + }, + is_v2: true, + marker: None, + continuation_token, + start_after, + }, + ) + .await + } else { + Err(Error::BadRequest(format!( + "Invalid endpoint: list-type={}", + list_type + ))) + } + } + Endpoint::ListMultipartUploads { + delimiter, + encoding_type, + key_marker, + max_uploads, + prefix, + upload_id_marker, + } => { + handle_list_multipart_upload( + garage, + &ListMultipartUploadsQuery { + common: ListQueryCommon { + bucket_name, + bucket_id, + delimiter: delimiter.map(|d| d.to_string()), + page_size: max_uploads.map(|p| p.clamp(1, 1000)).unwrap_or(1000), + prefix: prefix.unwrap_or_default(), + urlencode_resp: encoding_type.map(|e| e == "url").unwrap_or(false), + }, + key_marker, + upload_id_marker, + }, + ) + .await + } + Endpoint::ListParts { + key, + max_parts, + part_number_marker, + upload_id, + } => { + handle_list_parts( + garage, + &ListPartsQuery { + bucket_name, + bucket_id, + key, + upload_id, + part_number_marker: part_number_marker.map(|p| p.clamp(1, 10000)), + max_parts: max_parts.map(|p| p.clamp(1, 1000)).unwrap_or(1000), + }, + ) + .await + } + Endpoint::DeleteObjects {} => { + handle_delete_objects(garage, bucket_id, req, content_sha256).await + } + Endpoint::GetBucketWebsite {} => handle_get_website(&bucket).await, + Endpoint::PutBucketWebsite {} => { + handle_put_website(garage, bucket_id, req, content_sha256).await + } + Endpoint::DeleteBucketWebsite {} => handle_delete_website(garage, bucket_id).await, + Endpoint::GetBucketCors {} => handle_get_cors(&bucket).await, + Endpoint::PutBucketCors {} => { + handle_put_cors(garage, bucket_id, req, content_sha256).await + } + Endpoint::DeleteBucketCors {} => handle_delete_cors(garage, bucket_id).await, + endpoint => Err(Error::NotImplemented(endpoint.name().to_owned())), + }; + + // If request was a success and we have a CORS rule that applies to it, + // add the corresponding CORS headers to the response + let mut resp_ok = resp?; + if let Some(rule) = matching_cors_rule { + add_cors_headers(&mut resp_ok, rule) + .ok_or_internal_error("Invalid bucket CORS configuration")?; + } + + Ok(resp_ok) + } +} + +impl ApiEndpoint for S3ApiEndpoint { + fn name(&self) -> &'static str { + self.endpoint.name() + } + + fn add_span_attributes(&self, span: SpanRef<'_>) { + span.set_attribute(KeyValue::new( + "bucket", + self.bucket_name.clone().unwrap_or_default(), + )); + } +} diff --git a/src/api/s3_bucket.rs b/src/api/s3/bucket.rs similarity index 97% rename from src/api/s3_bucket.rs rename to src/api/s3/bucket.rs index 8a5407d3..93048a8c 100644 --- a/src/api/s3_bucket.rs +++ b/src/api/s3/bucket.rs @@ -7,15 +7,15 @@ use garage_model::bucket_alias_table::*; use garage_model::bucket_table::Bucket; use garage_model::garage::Garage; use garage_model::key_table::Key; -use garage_model::object_table::ObjectFilter; use garage_model::permission::BucketKeyPerm; +use garage_model::s3::object_table::ObjectFilter; use garage_table::util::*; use garage_util::crdt::*; use garage_util::data::*; use garage_util::time::*; use crate::error::*; -use crate::s3_xml; +use crate::s3::xml as s3_xml; use crate::signature::verify_signed_content; pub fn handle_get_bucket_location(garage: Arc) -> Result, Error> { @@ -230,7 +230,13 @@ pub async fn handle_delete_bucket( // Check bucket is empty let objects = garage .object_table - .get_range(&bucket_id, None, Some(ObjectFilter::IsData), 10) + .get_range( + &bucket_id, + None, + Some(ObjectFilter::IsData), + 10, + EnumerationOrder::Forward, + ) .await?; if !objects.is_empty() { return Err(Error::BucketNotEmpty); diff --git a/src/api/s3_copy.rs b/src/api/s3/copy.rs similarity index 98% rename from src/api/s3_copy.rs rename to src/api/s3/copy.rs index fc4707e2..4e94d887 100644 --- a/src/api/s3_copy.rs +++ b/src/api/s3/copy.rs @@ -12,16 +12,16 @@ use garage_table::*; use garage_util::data::*; use garage_util::time::*; -use garage_model::block_ref_table::*; use garage_model::garage::Garage; use garage_model::key_table::Key; -use garage_model::object_table::*; -use garage_model::version_table::*; +use garage_model::s3::block_ref_table::*; +use garage_model::s3::object_table::*; +use garage_model::s3::version_table::*; -use crate::api_server::{parse_bucket_key, resolve_bucket}; use crate::error::*; -use crate::s3_put::{decode_upload_id, get_headers}; -use crate::s3_xml::{self, xmlns_tag}; +use crate::helpers::{parse_bucket_key, resolve_bucket}; +use crate::s3::put::{decode_upload_id, get_headers}; +use crate::s3::xml::{self as s3_xml, xmlns_tag}; pub async fn handle_copy( garage: Arc, @@ -619,7 +619,7 @@ pub struct CopyPartResult { #[cfg(test)] mod tests { use super::*; - use crate::s3_xml::to_xml_with_header; + use crate::s3::xml::to_xml_with_header; #[test] fn copy_object_result() -> Result<(), Error> { diff --git a/src/api/s3_cors.rs b/src/api/s3/cors.rs similarity index 99% rename from src/api/s3_cors.rs rename to src/api/s3/cors.rs index ab77e23a..37ea2e43 100644 --- a/src/api/s3_cors.rs +++ b/src/api/s3/cors.rs @@ -10,7 +10,7 @@ use hyper::{header::HeaderName, Body, Method, Request, Response, StatusCode}; use serde::{Deserialize, Serialize}; use crate::error::*; -use crate::s3_xml::{to_xml_with_header, xmlns_tag, IntValue, Value}; +use crate::s3::xml::{to_xml_with_header, xmlns_tag, IntValue, Value}; use crate::signature::verify_signed_content; use garage_model::bucket_table::{Bucket, CorsRule as GarageCorsRule}; diff --git a/src/api/s3_delete.rs b/src/api/s3/delete.rs similarity index 98% rename from src/api/s3_delete.rs rename to src/api/s3/delete.rs index b243d982..1e3f1249 100644 --- a/src/api/s3_delete.rs +++ b/src/api/s3/delete.rs @@ -6,10 +6,10 @@ use garage_util::data::*; use garage_util::time::*; use garage_model::garage::Garage; -use garage_model::object_table::*; +use garage_model::s3::object_table::*; use crate::error::*; -use crate::s3_xml; +use crate::s3::xml as s3_xml; use crate::signature::verify_signed_content; async fn handle_delete_internal( diff --git a/src/api/s3_get.rs b/src/api/s3/get.rs similarity index 99% rename from src/api/s3_get.rs rename to src/api/s3/get.rs index 7f647e15..3edf22a6 100644 --- a/src/api/s3_get.rs +++ b/src/api/s3/get.rs @@ -14,8 +14,8 @@ use garage_table::EmptyKey; use garage_util::data::*; use garage_model::garage::Garage; -use garage_model::object_table::*; -use garage_model::version_table::*; +use garage_model::s3::object_table::*; +use garage_model::s3::version_table::*; use crate::error::*; diff --git a/src/api/s3_list.rs b/src/api/s3/list.rs similarity index 94% rename from src/api/s3_list.rs rename to src/api/s3/list.rs index 5852fc1b..e2848c57 100644 --- a/src/api/s3_list.rs +++ b/src/api/s3/list.rs @@ -10,15 +10,16 @@ use garage_util::error::Error as GarageError; use garage_util::time::*; use garage_model::garage::Garage; -use garage_model::object_table::*; -use garage_model::version_table::Version; +use garage_model::s3::object_table::*; +use garage_model::s3::version_table::Version; -use garage_table::EmptyKey; +use garage_table::{EmptyKey, EnumerationOrder}; use crate::encoding::*; use crate::error::*; -use crate::s3_put; -use crate::s3_xml; +use crate::helpers::key_after_prefix; +use crate::s3::put as s3_put; +use crate::s3::xml as s3_xml; const DUMMY_NAME: &str = "Dummy Key"; const DUMMY_KEY: &str = "GKDummyKey"; @@ -66,8 +67,14 @@ pub async fn handle_list( let io = |bucket, key, count| { let t = &garage.object_table; async move { - t.get_range(&bucket, key, Some(ObjectFilter::IsData), count) - .await + t.get_range( + &bucket, + key, + Some(ObjectFilter::IsData), + count, + EnumerationOrder::Forward, + ) + .await } }; @@ -165,8 +172,14 @@ pub async fn handle_list_multipart_upload( let io = |bucket, key, count| { let t = &garage.object_table; async move { - t.get_range(&bucket, key, Some(ObjectFilter::IsUploading), count) - .await + t.get_range( + &bucket, + key, + Some(ObjectFilter::IsUploading), + count, + EnumerationOrder::Forward, + ) + .await } }; @@ -923,39 +936,13 @@ fn uriencode_maybe(s: &str, yes: bool) -> s3_xml::Value { } } -const UTF8_BEFORE_LAST_CHAR: char = '\u{10FFFE}'; - -/// Compute the key after the prefix -fn key_after_prefix(pfx: &str) -> Option { - let mut next = pfx.to_string(); - while !next.is_empty() { - let tail = next.pop().unwrap(); - if tail >= char::MAX { - continue; - } - - // Circumvent a limitation of RangeFrom that overflow earlier than needed - // See: https://doc.rust-lang.org/core/ops/struct.RangeFrom.html - let new_tail = if tail == UTF8_BEFORE_LAST_CHAR { - char::MAX - } else { - (tail..).nth(1).unwrap() - }; - - next.push(new_tail); - return Some(next); - } - - None -} - /* * Unit tests of this module */ #[cfg(test)] mod tests { use super::*; - use garage_model::version_table::*; + use garage_model::s3::version_table::*; use garage_util::*; use std::iter::FromIterator; @@ -1002,39 +989,6 @@ mod tests { } } - #[test] - fn test_key_after_prefix() { - assert_eq!(UTF8_BEFORE_LAST_CHAR as u32, (char::MAX as u32) - 1); - assert_eq!(key_after_prefix("a/b/").unwrap().as_str(), "a/b0"); - assert_eq!(key_after_prefix("€").unwrap().as_str(), "₭"); - assert_eq!( - key_after_prefix("􏿽").unwrap().as_str(), - String::from(char::from_u32(0x10FFFE).unwrap()) - ); - - // When the last character is the biggest UTF8 char - let a = String::from_iter(['a', char::MAX].iter()); - assert_eq!(key_after_prefix(a.as_str()).unwrap().as_str(), "b"); - - // When all characters are the biggest UTF8 char - let b = String::from_iter([char::MAX; 3].iter()); - assert!(key_after_prefix(b.as_str()).is_none()); - - // Check utf8 surrogates - let c = String::from('\u{D7FF}'); - assert_eq!( - key_after_prefix(c.as_str()).unwrap().as_str(), - String::from('\u{E000}') - ); - - // Check the character before the biggest one - let d = String::from('\u{10FFFE}'); - assert_eq!( - key_after_prefix(d.as_str()).unwrap().as_str(), - String::from(char::MAX) - ); - } - #[test] fn test_common_prefixes() { let mut query = query(); diff --git a/src/api/s3/mod.rs b/src/api/s3/mod.rs new file mode 100644 index 00000000..3f5c1915 --- /dev/null +++ b/src/api/s3/mod.rs @@ -0,0 +1,14 @@ +pub mod api_server; + +mod bucket; +mod copy; +pub mod cors; +mod delete; +pub mod get; +mod list; +mod post_object; +mod put; +mod website; + +mod router; +pub mod xml; diff --git a/src/api/s3_post_object.rs b/src/api/s3/post_object.rs similarity index 98% rename from src/api/s3_post_object.rs rename to src/api/s3/post_object.rs index 585e0304..86fa7880 100644 --- a/src/api/s3_post_object.rs +++ b/src/api/s3/post_object.rs @@ -14,10 +14,10 @@ use serde::Deserialize; use garage_model::garage::Garage; -use crate::api_server::resolve_bucket; use crate::error::*; -use crate::s3_put::{get_headers, save_stream}; -use crate::s3_xml; +use crate::helpers::resolve_bucket; +use crate::s3::put::{get_headers, save_stream}; +use crate::s3::xml as s3_xml; use crate::signature::payload::{parse_date, verify_v4}; pub async fn handle_post_object( @@ -119,7 +119,15 @@ pub async fn handle_post_object( }; let date = parse_date(date)?; - let api_key = verify_v4(&garage, credential, &date, signature, policy.as_bytes()).await?; + let api_key = verify_v4( + &garage, + "s3", + credential, + &date, + signature, + policy.as_bytes(), + ) + .await?; let bucket_id = resolve_bucket(&garage, &bucket, &api_key).await?; diff --git a/src/api/s3_put.rs b/src/api/s3/put.rs similarity index 99% rename from src/api/s3_put.rs rename to src/api/s3/put.rs index ed0bf00b..89aa8d84 100644 --- a/src/api/s3_put.rs +++ b/src/api/s3/put.rs @@ -14,13 +14,13 @@ use garage_util::error::Error as GarageError; use garage_util::time::*; use garage_block::manager::INLINE_THRESHOLD; -use garage_model::block_ref_table::*; use garage_model::garage::Garage; -use garage_model::object_table::*; -use garage_model::version_table::*; +use garage_model::s3::block_ref_table::*; +use garage_model::s3::object_table::*; +use garage_model::s3::version_table::*; use crate::error::*; -use crate::s3_xml; +use crate::s3::xml as s3_xml; use crate::signature::verify_signed_content; pub async fn handle_put( diff --git a/src/api/s3_router.rs b/src/api/s3/router.rs similarity index 81% rename from src/api/s3_router.rs rename to src/api/s3/router.rs index 95a7eceb..0525c649 100644 --- a/src/api/s3_router.rs +++ b/src/api/s3/router.rs @@ -5,127 +5,10 @@ use std::borrow::Cow; use hyper::header::HeaderValue; use hyper::{HeaderMap, Method, Request}; -/// This macro is used to generate very repetitive match {} blocks in this module -/// It is _not_ made to be used anywhere else -macro_rules! s3_match { - (@match $enum:expr , [ $($endpoint:ident,)* ]) => {{ - // usage: s3_match {@match my_enum, [ VariantWithField1, VariantWithField2 ..] } - // returns true if the variant was one of the listed variants, false otherwise. - use Endpoint::*; - match $enum { - $( - $endpoint { .. } => true, - )* - _ => false - } - }}; - (@extract $enum:expr , $param:ident, [ $($endpoint:ident,)* ]) => {{ - // usage: s3_match {@extract my_enum, field_name, [ VariantWithField1, VariantWithField2 ..] } - // returns Some(field_value), or None if the variant was not one of the listed variants. - use Endpoint::*; - match $enum { - $( - $endpoint {$param, ..} => Some($param), - )* - _ => None - } - }}; - (@gen_parser ($keyword:expr, $key:expr, $query:expr, $header:expr), - key: [$($kw_k:ident $(if $required_k:ident)? $(header $header_k:expr)? => $api_k:ident $(($($conv_k:ident :: $param_k:ident),*))?,)*], - no_key: [$($kw_nk:ident $(if $required_nk:ident)? $(if_header $header_nk:expr)? => $api_nk:ident $(($($conv_nk:ident :: $param_nk:ident),*))?,)*]) => {{ - // usage: s3_match {@gen_parser (keyword, key, query, header), - // key: [ - // SOME_KEYWORD => VariantWithKey, - // ... - // ], - // no_key: [ - // SOME_KEYWORD => VariantWithoutKey, - // ... - // ] - // } - // See in from_{method} for more detailed usage. - use Endpoint::*; - use keywords::*; - match ($keyword, !$key.is_empty()){ - $( - ($kw_k, true) if true $(&& $query.$required_k.is_some())? $(&& $header.contains_key($header_k))? => Ok($api_k { - key: $key, - $($( - $param_k: s3_match!(@@parse_param $query, $conv_k, $param_k), - )*)? - }), - )* - $( - ($kw_nk, false) $(if $query.$required_nk.is_some())? $(if $header.contains($header_nk))? => Ok($api_nk { - $($( - $param_nk: s3_match!(@@parse_param $query, $conv_nk, $param_nk), - )*)? - }), - )* - (kw, _) => Err(Error::BadRequest(format!("Invalid endpoint: {}", kw))) - } - }}; +use crate::helpers::Authorization; +use crate::router_macros::{generateQueryParameters, router_match}; - (@@parse_param $query:expr, query_opt, $param:ident) => {{ - // extract optional query parameter - $query.$param.take().map(|param| param.into_owned()) - }}; - (@@parse_param $query:expr, query, $param:ident) => {{ - // extract mendatory query parameter - $query.$param.take().ok_or_bad_request("Missing argument for endpoint")?.into_owned() - }}; - (@@parse_param $query:expr, opt_parse, $param:ident) => {{ - // extract and parse optional query parameter - // missing parameter is file, however parse error is reported as an error - $query.$param - .take() - .map(|param| param.parse()) - .transpose() - .map_err(|_| Error::BadRequest("Failed to parse query parameter".to_owned()))? - }}; - (@@parse_param $query:expr, parse, $param:ident) => {{ - // extract and parse mandatory query parameter - // both missing and un-parseable parameters are reported as errors - $query.$param.take().ok_or_bad_request("Missing argument for endpoint")? - .parse() - .map_err(|_| Error::BadRequest("Failed to parse query parameter".to_owned()))? - }}; - (@func - $(#[$doc:meta])* - pub enum Endpoint { - $( - $(#[$outer:meta])* - $variant:ident $({ - $($name:ident: $ty:ty,)* - })?, - )* - }) => { - $(#[$doc])* - pub enum Endpoint { - $( - $(#[$outer])* - $variant $({ - $($name: $ty, )* - })?, - )* - } - impl Endpoint { - pub fn name(&self) -> &'static str { - match self { - $(Endpoint::$variant $({ $($name: _,)* .. })? => stringify!($variant),)* - } - } - } - }; - (@if ($($cond:tt)+) then ($($then:tt)*) else ($($else:tt)*)) => { - $($then)* - }; - (@if () then ($($then:tt)*) else ($($else:tt)*)) => { - $($else)* - }; -} - -s3_match! {@func +router_match! {@func /// List of all S3 API endpoints. /// @@ -471,7 +354,7 @@ impl Endpoint { /// Determine which endpoint a request is for, knowing it is a GET. fn from_get(key: String, query: &mut QueryParameters<'_>) -> Result { - s3_match! { + router_match! { @gen_parser (query.keyword.take().unwrap_or_default().as_ref(), key, query, None), key: [ @@ -528,7 +411,7 @@ impl Endpoint { /// Determine which endpoint a request is for, knowing it is a HEAD. fn from_head(key: String, query: &mut QueryParameters<'_>) -> Result { - s3_match! { + router_match! { @gen_parser (query.keyword.take().unwrap_or_default().as_ref(), key, query, None), key: [ @@ -542,7 +425,7 @@ impl Endpoint { /// Determine which endpoint a request is for, knowing it is a POST. fn from_post(key: String, query: &mut QueryParameters<'_>) -> Result { - s3_match! { + router_match! { @gen_parser (query.keyword.take().unwrap_or_default().as_ref(), key, query, None), key: [ @@ -564,7 +447,7 @@ impl Endpoint { query: &mut QueryParameters<'_>, headers: &HeaderMap, ) -> Result { - s3_match! { + router_match! { @gen_parser (query.keyword.take().unwrap_or_default().as_ref(), key, query, headers), key: [ @@ -606,7 +489,7 @@ impl Endpoint { /// Determine which endpoint a request is for, knowing it is a DELETE. fn from_delete(key: String, query: &mut QueryParameters<'_>) -> Result { - s3_match! { + router_match! { @gen_parser (query.keyword.take().unwrap_or_default().as_ref(), key, query, None), key: [ @@ -636,7 +519,7 @@ impl Endpoint { /// Get the key the request target. Returns None for requests which don't use a key. #[allow(dead_code)] pub fn get_key(&self) -> Option<&str> { - s3_match! { + router_match! { @extract self, key, @@ -673,7 +556,7 @@ impl Endpoint { if let Endpoint::ListBuckets = self { return Authorization::None; }; - let readonly = s3_match! { + let readonly = router_match! { @match self, [ @@ -717,7 +600,7 @@ impl Endpoint { SelectObjectContent, ] }; - let owner = s3_match! { + let owner = router_match! { @match self, [ @@ -740,87 +623,6 @@ impl Endpoint { } } -/// What kind of authorization is required to perform a given action -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Authorization { - /// No authorization is required - None, - /// Having Read permission on bucket - Read, - /// Having Write permission on bucket - Write, - /// Having Owner permission on bucket - Owner, -} - -/// This macro is used to generate part of the code in this module. It must be called only one, and -/// is useless outside of this module. -macro_rules! generateQueryParameters { - ( $($rest:expr => $name:ident),* ) => { - /// Struct containing all query parameters used in endpoints. Think of it as an HashMap, - /// but with keys statically known. - #[derive(Debug, Default)] - struct QueryParameters<'a> { - keyword: Option>, - $( - $name: Option>, - )* - } - - impl<'a> QueryParameters<'a> { - /// Build this struct from the query part of an URI. - fn from_query(query: &'a str) -> Result { - let mut res: Self = Default::default(); - for (k, v) in url::form_urlencoded::parse(query.as_bytes()) { - let repeated = match k.as_ref() { - $( - $rest => if !v.is_empty() { - res.$name.replace(v).is_some() - } else { - false - }, - )* - _ => { - if k.starts_with("response-") || k.starts_with("X-Amz-") { - false - } else if v.as_ref().is_empty() { - if res.keyword.replace(k).is_some() { - return Err(Error::BadRequest("Multiple keywords".to_owned())); - } - continue; - } else { - debug!("Received an unknown query parameter: '{}'", k); - false - } - } - }; - if repeated { - return Err(Error::BadRequest(format!( - "Query parameter repeated: '{}'", - k - ))); - } - } - Ok(res) - } - - /// Get an error message in case not all parameters where used when extracting them to - /// build an Enpoint variant - fn nonempty_message(&self) -> Option<&str> { - if self.keyword.is_some() { - Some("Keyword not used") - } $( - else if self.$name.is_some() { - Some(concat!("'", $rest, "'")) - } - )* else { - None - } - } - } - } -} - // parameter name => struct field generateQueryParameters! { "continuation-token" => continuation_token, diff --git a/src/api/s3_website.rs b/src/api/s3/website.rs similarity index 99% rename from src/api/s3_website.rs rename to src/api/s3/website.rs index b464dd45..561130dc 100644 --- a/src/api/s3_website.rs +++ b/src/api/s3/website.rs @@ -5,7 +5,7 @@ use hyper::{Body, Request, Response, StatusCode}; use serde::{Deserialize, Serialize}; use crate::error::*; -use crate::s3_xml::{to_xml_with_header, xmlns_tag, IntValue, Value}; +use crate::s3::xml::{to_xml_with_header, xmlns_tag, IntValue, Value}; use crate::signature::verify_signed_content; use garage_model::bucket_table::*; diff --git a/src/api/s3_xml.rs b/src/api/s3/xml.rs similarity index 100% rename from src/api/s3_xml.rs rename to src/api/s3/xml.rs diff --git a/src/api/signature/mod.rs b/src/api/signature/mod.rs index ebdee6da..5646f4fa 100644 --- a/src/api/signature/mod.rs +++ b/src/api/signature/mod.rs @@ -42,6 +42,11 @@ pub fn signing_hmac( Ok(hmac) } -pub fn compute_scope(datetime: &DateTime, region: &str) -> String { - format!("{}/{}/s3/aws4_request", datetime.format(SHORT_DATE), region,) +pub fn compute_scope(datetime: &DateTime, region: &str, service: &str) -> String { + format!( + "{}/{}/{}/aws4_request", + datetime.format(SHORT_DATE), + region, + service + ) } diff --git a/src/api/signature/payload.rs b/src/api/signature/payload.rs index 2a41b307..9137dd2d 100644 --- a/src/api/signature/payload.rs +++ b/src/api/signature/payload.rs @@ -11,14 +11,15 @@ use garage_util::data::Hash; use garage_model::garage::Garage; use garage_model::key_table::*; -use super::signing_hmac; -use super::{LONG_DATETIME, SHORT_DATE}; +use super::LONG_DATETIME; +use super::{compute_scope, signing_hmac}; use crate::encoding::uri_encode; use crate::error::*; pub async fn check_payload_signature( garage: &Garage, + service: &str, request: &Request, ) -> Result<(Option, Option), Error> { let mut headers = HashMap::new(); @@ -64,6 +65,7 @@ pub async fn check_payload_signature( let key = verify_v4( garage, + service, &authorization.credential, &authorization.date, &authorization.signature, @@ -281,6 +283,7 @@ pub fn parse_date(date: &str) -> Result, Error> { pub async fn verify_v4( garage: &Garage, + service: &str, credential: &str, date: &DateTime, signature: &str, @@ -288,11 +291,7 @@ pub async fn verify_v4( ) -> Result { let (key_id, scope) = parse_credential(credential)?; - let scope_expected = format!( - "{}/{}/s3/aws4_request", - date.format(SHORT_DATE), - garage.config.s3_api.s3_region - ); + let scope_expected = compute_scope(date, &garage.config.s3_api.s3_region, service); if scope != scope_expected { return Err(Error::AuthorizationHeaderMalformed(scope.to_string())); } @@ -309,7 +308,7 @@ pub async fn verify_v4( date, &key_p.secret_key, &garage.config.s3_api.s3_region, - "s3", + service, ) .ok_or_internal_error("Unable to build signing HMAC")?; hmac.update(payload); diff --git a/src/api/signature/streaming.rs b/src/api/signature/streaming.rs index 969a45d6..ded9d993 100644 --- a/src/api/signature/streaming.rs +++ b/src/api/signature/streaming.rs @@ -1,19 +1,68 @@ use std::pin::Pin; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, NaiveDateTime, Utc}; use futures::prelude::*; use futures::task; +use garage_model::key_table::Key; +use hmac::Mac; use hyper::body::Bytes; +use hyper::{Body, Request}; use garage_util::data::Hash; -use hmac::Mac; -use super::sha256sum; -use super::HmacSha256; -use super::LONG_DATETIME; +use super::{compute_scope, sha256sum, HmacSha256, LONG_DATETIME}; use crate::error::*; +pub fn parse_streaming_body( + api_key: &Key, + req: Request, + content_sha256: &mut Option, + region: &str, + service: &str, +) -> Result, Error> { + match req.headers().get("x-amz-content-sha256") { + Some(header) if header == "STREAMING-AWS4-HMAC-SHA256-PAYLOAD" => { + let signature = content_sha256 + .take() + .ok_or_bad_request("No signature provided")?; + + let secret_key = &api_key + .state + .as_option() + .ok_or_internal_error("Deleted key state")? + .secret_key; + + let date = req + .headers() + .get("x-amz-date") + .ok_or_bad_request("Missing X-Amz-Date field")? + .to_str()?; + let date: NaiveDateTime = NaiveDateTime::parse_from_str(date, LONG_DATETIME) + .ok_or_bad_request("Invalid date")?; + let date: DateTime = DateTime::from_utc(date, Utc); + + let scope = compute_scope(&date, region, service); + let signing_hmac = crate::signature::signing_hmac(&date, secret_key, region, service) + .ok_or_internal_error("Unable to build signing HMAC")?; + + Ok(req.map(move |body| { + Body::wrap_stream( + SignedPayloadStream::new( + body.map_err(Error::from), + signing_hmac, + date, + &scope, + signature, + ) + .map_err(Error::from), + ) + })) + } + _ => Ok(req), + } +} + /// Result of `sha256("")` const EMPTY_STRING_HEX_DIGEST: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; @@ -295,7 +344,7 @@ mod tests { .with_timezone(&Utc); let secret_key = "test"; let region = "test"; - let scope = crate::signature::compute_scope(&datetime, region); + let scope = crate::signature::compute_scope(&datetime, region, "s3"); let signing_hmac = crate::signature::signing_hmac(&datetime, secret_key, region, "s3").unwrap(); diff --git a/src/block/manager.rs b/src/block/manager.rs index 1c04a335..9b2d9cad 100644 --- a/src/block/manager.rs +++ b/src/block/manager.rs @@ -132,7 +132,7 @@ impl BlockManager { let endpoint = system .netapp - .endpoint("garage_model/block.rs/Rpc".to_string()); + .endpoint("garage_block/manager.rs/Rpc".to_string()); let manager_locked = BlockManagerLocked(); diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index 59f402ff..3b69d7bc 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -63,3 +63,11 @@ hyper = { version = "0.14", features = ["client", "http1", "runtime"] } sha2 = "0.9" static_init = "1.0" +assert-json-diff = "2.0" +serde_json = "1.0" +base64 = "0.13" + + +[features] +kubernetes-discovery = [ "garage_rpc/kubernetes-discovery" ] +k2v = [ "garage_util/k2v", "garage_api/k2v" ] diff --git a/src/garage/admin.rs b/src/garage/admin.rs index 0b20bb20..af0c3f22 100644 --- a/src/garage/admin.rs +++ b/src/garage/admin.rs @@ -21,8 +21,8 @@ use garage_model::garage::Garage; use garage_model::helper::error::{Error, OkOrBadRequest}; use garage_model::key_table::*; use garage_model::migrate::Migrate; -use garage_model::object_table::ObjectFilter; use garage_model::permission::*; +use garage_model::s3::object_table::ObjectFilter; use crate::cli::*; use crate::repair::Repair; @@ -80,7 +80,13 @@ impl AdminRpcHandler { let buckets = self .garage .bucket_table - .get_range(&EmptyKey, None, Some(DeletedFilter::NotDeleted), 10000) + .get_range( + &EmptyKey, + None, + Some(DeletedFilter::NotDeleted), + 10000, + EnumerationOrder::Forward, + ) .await?; Ok(AdminRpc::BucketList(buckets)) } @@ -210,7 +216,13 @@ impl AdminRpcHandler { let objects = self .garage .object_table - .get_range(&bucket_id, None, Some(ObjectFilter::IsData), 10) + .get_range( + &bucket_id, + None, + Some(ObjectFilter::IsData), + 10, + EnumerationOrder::Forward, + ) .await?; if !objects.is_empty() { return Err(Error::BadRequest(format!( @@ -445,6 +457,7 @@ impl AdminRpcHandler { None, Some(KeyFilter::Deleted(DeletedFilter::NotDeleted)), 10000, + EnumerationOrder::Forward, ) .await? .iter() diff --git a/src/garage/cli/cmd.rs b/src/garage/cli/cmd.rs index a90277a0..2a799868 100644 --- a/src/garage/cli/cmd.rs +++ b/src/garage/cli/cmd.rs @@ -85,13 +85,14 @@ pub async fn cmd_status(rpc_cli: &Endpoint, rpc_host: NodeID) -> format_table(healthy_nodes); let status_keys = status.iter().map(|adv| adv.id).collect::>(); - let failure_case_1 = status.iter().any(|adv| !adv.is_up); + let failure_case_1 = status + .iter() + .any(|adv| !adv.is_up && matches!(layout.roles.get(&adv.id), Some(NodeRoleV(Some(_))))); let failure_case_2 = layout .roles .items() .iter() - .filter(|(_, _, v)| v.0.is_some()) - .any(|(id, _, _)| !status_keys.contains(id)); + .any(|(id, _, v)| !status_keys.contains(id) && v.0.is_some()); if failure_case_1 || failure_case_2 { println!("\n==== FAILED NODES ===="); let mut failed_nodes = diff --git a/src/garage/repair.rs b/src/garage/repair.rs index 3666ca8f..830eac71 100644 --- a/src/garage/repair.rs +++ b/src/garage/repair.rs @@ -2,10 +2,10 @@ use std::sync::Arc; use tokio::sync::watch; -use garage_model::block_ref_table::*; use garage_model::garage::Garage; -use garage_model::object_table::*; -use garage_model::version_table::*; +use garage_model::s3::block_ref_table::*; +use garage_model::s3::object_table::*; +use garage_model::s3::version_table::*; use garage_table::*; use garage_util::error::Error; diff --git a/src/garage/server.rs b/src/garage/server.rs index 58c9e782..24bb25b3 100644 --- a/src/garage/server.rs +++ b/src/garage/server.rs @@ -8,10 +8,13 @@ use garage_util::error::Error; use garage_admin::metrics::*; use garage_admin::tracing_setup::*; -use garage_api::run_api_server; +use garage_api::s3::api_server::S3ApiServer; use garage_model::garage::Garage; use garage_web::run_web_server; +#[cfg(feature = "k2v")] +use garage_api::k2v::api_server::K2VApiServer; + use crate::admin::*; async fn wait_from(mut chan: watch::Receiver) { @@ -56,12 +59,21 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> { info!("Create admin RPC handler..."); AdminRpcHandler::new(garage.clone()); - info!("Initializing API server..."); - let api_server = tokio::spawn(run_api_server( + info!("Initializing S3 API server..."); + let s3_api_server = tokio::spawn(S3ApiServer::run( garage.clone(), wait_from(watch_cancel.clone()), )); + #[cfg(feature = "k2v")] + let k2v_api_server = { + info!("Initializing K2V API server..."); + tokio::spawn(K2VApiServer::run( + garage.clone(), + wait_from(watch_cancel.clone()), + )) + }; + info!("Initializing web server..."); let web_server = tokio::spawn(run_web_server( garage.clone(), @@ -80,8 +92,12 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> { // Stuff runs // When a cancel signal is sent, stuff stops - if let Err(e) = api_server.await? { - warn!("API server exited with error: {}", e); + if let Err(e) = s3_api_server.await? { + warn!("S3 API server exited with error: {}", e); + } + #[cfg(feature = "k2v")] + if let Err(e) = k2v_api_server.await? { + warn!("K2V API server exited with error: {}", e); } if let Err(e) = web_server.await? { warn!("Web server exited with error: {}", e); diff --git a/src/garage/tests/common/client.rs b/src/garage/tests/common/client.rs index c5ddc6e5..212588b5 100644 --- a/src/garage/tests/common/client.rs +++ b/src/garage/tests/common/client.rs @@ -10,7 +10,7 @@ pub fn build_client(instance: &Instance) -> Client { None, "garage-integ-test", ); - let endpoint = Endpoint::immutable(instance.uri()); + let endpoint = Endpoint::immutable(instance.s3_uri()); let config = Config::builder() .region(super::REGION) diff --git a/src/garage/tests/common/custom_requester.rs b/src/garage/tests/common/custom_requester.rs index 580691a1..1700cc90 100644 --- a/src/garage/tests/common/custom_requester.rs +++ b/src/garage/tests/common/custom_requester.rs @@ -17,14 +17,25 @@ use garage_api::signature; pub struct CustomRequester { key: Key, uri: Uri, + service: &'static str, client: Client, } impl CustomRequester { - pub fn new(instance: &Instance) -> Self { + pub fn new_s3(instance: &Instance) -> Self { CustomRequester { key: instance.key.clone(), - uri: instance.uri(), + uri: instance.s3_uri(), + service: "s3", + client: Client::new(), + } + } + + pub fn new_k2v(instance: &Instance) -> Self { + CustomRequester { + key: instance.key.clone(), + uri: instance.k2v_uri(), + service: "k2v", client: Client::new(), } } @@ -32,6 +43,7 @@ impl CustomRequester { pub fn builder(&self, bucket: String) -> RequestBuilder<'_> { RequestBuilder { requester: self, + service: self.service, bucket, method: Method::GET, path: String::new(), @@ -47,6 +59,7 @@ impl CustomRequester { pub struct RequestBuilder<'a> { requester: &'a CustomRequester, + service: &'static str, bucket: String, method: Method, path: String, @@ -59,13 +72,17 @@ pub struct RequestBuilder<'a> { } impl<'a> RequestBuilder<'a> { + pub fn service(&mut self, service: &'static str) -> &mut Self { + self.service = service; + self + } pub fn method(&mut self, method: Method) -> &mut Self { self.method = method; self } - pub fn path(&mut self, path: String) -> &mut Self { - self.path = path; + pub fn path(&mut self, path: impl ToString) -> &mut Self { + self.path = path.to_string(); self } @@ -74,16 +91,38 @@ impl<'a> RequestBuilder<'a> { self } + pub fn query_param(&mut self, param: T, value: Option) -> &mut Self + where + T: ToString, + U: ToString, + { + self.query_params + .insert(param.to_string(), value.as_ref().map(ToString::to_string)); + self + } + pub fn signed_headers(&mut self, signed_headers: HashMap) -> &mut Self { self.signed_headers = signed_headers; self } + pub fn signed_header(&mut self, name: impl ToString, value: impl ToString) -> &mut Self { + self.signed_headers + .insert(name.to_string(), value.to_string()); + self + } + pub fn unsigned_headers(&mut self, unsigned_headers: HashMap) -> &mut Self { self.unsigned_headers = unsigned_headers; self } + pub fn unsigned_header(&mut self, name: impl ToString, value: impl ToString) -> &mut Self { + self.unsigned_headers + .insert(name.to_string(), value.to_string()); + self + } + pub fn body(&mut self, body: Vec) -> &mut Self { self.body = body; self @@ -106,24 +145,24 @@ impl<'a> RequestBuilder<'a> { let query = query_param_to_string(&self.query_params); let (host, path) = if self.vhost_style { ( - format!("{}.s3.garage", self.bucket), + format!("{}.{}.garage", self.bucket, self.service), format!("{}{}", self.path, query), ) } else { ( - "s3.garage".to_owned(), + format!("{}.garage", self.service), format!("{}/{}{}", self.bucket, self.path, query), ) }; let uri = format!("{}{}", self.requester.uri, path); let now = Utc::now(); - let scope = signature::compute_scope(&now, super::REGION.as_ref()); + let scope = signature::compute_scope(&now, super::REGION.as_ref(), self.service); let mut signer = signature::signing_hmac( &now, &self.requester.key.secret, super::REGION.as_ref(), - "s3", + self.service, ) .unwrap(); let streaming_signer = signer.clone(); diff --git a/src/garage/tests/common/garage.rs b/src/garage/tests/common/garage.rs index 88c51501..44d727f9 100644 --- a/src/garage/tests/common/garage.rs +++ b/src/garage/tests/common/garage.rs @@ -22,7 +22,9 @@ pub struct Instance { process: process::Child, pub path: PathBuf, pub key: Key, - pub api_port: u16, + pub s3_port: u16, + pub k2v_port: u16, + pub web_port: u16, } impl Instance { @@ -58,9 +60,12 @@ rpc_secret = "{secret}" [s3_api] s3_region = "{region}" -api_bind_addr = "127.0.0.1:{api_port}" +api_bind_addr = "127.0.0.1:{s3_port}" root_domain = ".s3.garage" +[k2v_api] +api_bind_addr = "127.0.0.1:{k2v_port}" + [s3_web] bind_addr = "127.0.0.1:{web_port}" root_domain = ".web.garage" @@ -72,10 +77,11 @@ api_bind_addr = "127.0.0.1:{admin_port}" path = path.display(), secret = GARAGE_TEST_SECRET, region = super::REGION, - api_port = port, - rpc_port = port + 1, - web_port = port + 2, - admin_port = port + 3, + s3_port = port, + k2v_port = port + 1, + rpc_port = port + 2, + web_port = port + 3, + admin_port = port + 4, ); fs::write(path.join("config.toml"), config).expect("Could not write garage config file"); @@ -88,7 +94,7 @@ api_bind_addr = "127.0.0.1:{admin_port}" .arg("server") .stdout(stdout) .stderr(stderr) - .env("RUST_LOG", "garage=info,garage_api=debug") + .env("RUST_LOG", "garage=info,garage_api=trace") .spawn() .expect("Could not start garage"); @@ -96,7 +102,9 @@ api_bind_addr = "127.0.0.1:{admin_port}" process: child, path, key: Key::default(), - api_port: port, + s3_port: port, + k2v_port: port + 1, + web_port: port + 3, } } @@ -147,8 +155,14 @@ api_bind_addr = "127.0.0.1:{admin_port}" String::from_utf8(output.stdout).unwrap() } - pub fn uri(&self) -> http::Uri { - format!("http://127.0.0.1:{api_port}", api_port = self.api_port) + pub fn s3_uri(&self) -> http::Uri { + format!("http://127.0.0.1:{s3_port}", s3_port = self.s3_port) + .parse() + .expect("Could not build garage endpoint URI") + } + + pub fn k2v_uri(&self) -> http::Uri { + format!("http://127.0.0.1:{k2v_port}", k2v_port = self.k2v_port) .parse() .expect("Could not build garage endpoint URI") } diff --git a/src/garage/tests/common/mod.rs b/src/garage/tests/common/mod.rs index 8f88c731..28874b02 100644 --- a/src/garage/tests/common/mod.rs +++ b/src/garage/tests/common/mod.rs @@ -17,18 +17,27 @@ pub struct Context { pub garage: &'static garage::Instance, pub client: Client, pub custom_request: CustomRequester, + pub k2v: K2VContext, +} + +pub struct K2VContext { + pub request: CustomRequester, } impl Context { fn new() -> Self { let garage = garage::instance(); let client = client::build_client(garage); - let custom_request = CustomRequester::new(garage); + let custom_request = CustomRequester::new_s3(garage); + let k2v_request = CustomRequester::new_k2v(garage); Context { garage, client, custom_request, + k2v: K2VContext { + request: k2v_request, + }, } } diff --git a/src/garage/tests/k2v/batch.rs b/src/garage/tests/k2v/batch.rs new file mode 100644 index 00000000..1182a298 --- /dev/null +++ b/src/garage/tests/k2v/batch.rs @@ -0,0 +1,525 @@ +use std::collections::HashMap; + +use crate::common; + +use assert_json_diff::assert_json_eq; +use serde_json::json; + +use super::json_body; +use hyper::Method; + +#[tokio::test] +async fn test_batch() { + let ctx = common::context(); + let bucket = ctx.create_bucket("test-k2v-batch"); + + let mut values = HashMap::new(); + values.insert("a", "initial test 1"); + values.insert("b", "initial test 2"); + values.insert("c", "initial test 3"); + values.insert("d.1", "initial test 4"); + values.insert("d.2", "initial test 5"); + values.insert("e", "initial test 6"); + let mut ct = HashMap::new(); + + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .body( + format!( + r#"[ + {{"pk": "root", "sk": "a", "ct": null, "v": "{}"}}, + {{"pk": "root", "sk": "b", "ct": null, "v": "{}"}}, + {{"pk": "root", "sk": "c", "ct": null, "v": "{}"}}, + {{"pk": "root", "sk": "d.1", "ct": null, "v": "{}"}}, + {{"pk": "root", "sk": "d.2", "ct": null, "v": "{}"}}, + {{"pk": "root", "sk": "e", "ct": null, "v": "{}"}} + ]"#, + base64::encode(values.get(&"a").unwrap()), + base64::encode(values.get(&"b").unwrap()), + base64::encode(values.get(&"c").unwrap()), + base64::encode(values.get(&"d.1").unwrap()), + base64::encode(values.get(&"d.2").unwrap()), + base64::encode(values.get(&"e").unwrap()), + ) + .into_bytes(), + ) + .method(Method::POST) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + + for sk in ["a", "b", "c", "d.1", "d.2", "e"] { + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some(sk)) + .signed_header("accept", "*/*") + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + assert_eq!( + res.headers().get("content-type").unwrap().to_str().unwrap(), + "application/octet-stream" + ); + ct.insert( + sk, + res.headers() + .get("x-garage-causality-token") + .unwrap() + .to_str() + .unwrap() + .to_string(), + ); + let res_body = hyper::body::to_bytes(res.into_body()) + .await + .unwrap() + .to_vec(); + assert_eq!(res_body, values.get(sk).unwrap().as_bytes()); + } + + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .query_param("search", Option::<&str>::None) + .body( + br#"[ + {"partitionKey": "root"}, + {"partitionKey": "root", "start": "c"}, + {"partitionKey": "root", "start": "c", "reverse": true, "end": "a"}, + {"partitionKey": "root", "limit": 1}, + {"partitionKey": "root", "prefix": "d"} + ]"# + .to_vec(), + ) + .method(Method::POST) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + let json_res = json_body(res).await; + assert_json_eq!( + json_res, + json!([ + { + "partitionKey": "root", + "prefix": null, + "start": null, + "end": null, + "limit": null, + "reverse": false, + "conflictsOnly": false, + "tombstones": false, + "singleItem": false, + "items": [ + {"sk": "a", "ct": ct.get("a").unwrap(), "v": [base64::encode(values.get("a").unwrap())]}, + {"sk": "b", "ct": ct.get("b").unwrap(), "v": [base64::encode(values.get("b").unwrap())]}, + {"sk": "c", "ct": ct.get("c").unwrap(), "v": [base64::encode(values.get("c").unwrap())]}, + {"sk": "d.1", "ct": ct.get("d.1").unwrap(), "v": [base64::encode(values.get("d.1").unwrap())]}, + {"sk": "d.2", "ct": ct.get("d.2").unwrap(), "v": [base64::encode(values.get("d.2").unwrap())]}, + {"sk": "e", "ct": ct.get("e").unwrap(), "v": [base64::encode(values.get("e").unwrap())]} + ], + "more": false, + "nextStart": null, + }, + { + "partitionKey": "root", + "prefix": null, + "start": "c", + "end": null, + "limit": null, + "reverse": false, + "conflictsOnly": false, + "tombstones": false, + "singleItem": false, + "items": [ + {"sk": "c", "ct": ct.get("c").unwrap(), "v": [base64::encode(values.get("c").unwrap())]}, + {"sk": "d.1", "ct": ct.get("d.1").unwrap(), "v": [base64::encode(values.get("d.1").unwrap())]}, + {"sk": "d.2", "ct": ct.get("d.2").unwrap(), "v": [base64::encode(values.get("d.2").unwrap())]}, + {"sk": "e", "ct": ct.get("e").unwrap(), "v": [base64::encode(values.get("e").unwrap())]} + ], + "more": false, + "nextStart": null, + }, + { + "partitionKey": "root", + "prefix": null, + "start": "c", + "end": "a", + "limit": null, + "reverse": true, + "conflictsOnly": false, + "tombstones": false, + "singleItem": false, + "items": [ + {"sk": "c", "ct": ct.get("c").unwrap(), "v": [base64::encode(values.get("c").unwrap())]}, + {"sk": "b", "ct": ct.get("b").unwrap(), "v": [base64::encode(values.get("b").unwrap())]}, + ], + "more": false, + "nextStart": null, + }, + { + "partitionKey": "root", + "prefix": null, + "start": null, + "end": null, + "limit": 1, + "reverse": false, + "conflictsOnly": false, + "tombstones": false, + "singleItem": false, + "items": [ + {"sk": "a", "ct": ct.get("a").unwrap(), "v": [base64::encode(values.get("a").unwrap())]} + ], + "more": true, + "nextStart": "b", + }, + { + "partitionKey": "root", + "prefix": "d", + "start": null, + "end": null, + "limit": null, + "reverse": false, + "conflictsOnly": false, + "tombstones": false, + "singleItem": false, + "items": [ + {"sk": "d.1", "ct": ct.get("d.1").unwrap(), "v": [base64::encode(values.get("d.1").unwrap())]}, + {"sk": "d.2", "ct": ct.get("d.2").unwrap(), "v": [base64::encode(values.get("d.2").unwrap())]} + ], + "more": false, + "nextStart": null, + }, + ]) + ); + + // Insert some new values + values.insert("c'", "new test 3"); + values.insert("d.1'", "new test 4"); + values.insert("d.2'", "new test 5"); + + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .body( + format!( + r#"[ + {{"pk": "root", "sk": "b", "ct": "{}", "v": null}}, + {{"pk": "root", "sk": "c", "ct": null, "v": "{}"}}, + {{"pk": "root", "sk": "d.1", "ct": "{}", "v": "{}"}}, + {{"pk": "root", "sk": "d.2", "ct": null, "v": "{}"}} + ]"#, + ct.get(&"b").unwrap(), + base64::encode(values.get(&"c'").unwrap()), + ct.get(&"d.1").unwrap(), + base64::encode(values.get(&"d.1'").unwrap()), + base64::encode(values.get(&"d.2'").unwrap()), + ) + .into_bytes(), + ) + .method(Method::POST) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + + for sk in ["b", "c", "d.1", "d.2"] { + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some(sk)) + .signed_header("accept", "*/*") + .send() + .await + .unwrap(); + if sk == "b" { + assert_eq!(res.status(), 204); + } else { + assert_eq!(res.status(), 200); + } + ct.insert( + sk, + res.headers() + .get("x-garage-causality-token") + .unwrap() + .to_str() + .unwrap() + .to_string(), + ); + } + + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .query_param("search", Option::<&str>::None) + .body( + br#"[ + {"partitionKey": "root"}, + {"partitionKey": "root", "prefix": "d"}, + {"partitionKey": "root", "prefix": "d.", "end": "d.2"}, + {"partitionKey": "root", "prefix": "d.", "limit": 1}, + {"partitionKey": "root", "prefix": "d.", "start": "d.2", "limit": 1}, + {"partitionKey": "root", "prefix": "d.", "reverse": true}, + {"partitionKey": "root", "prefix": "d.", "start": "d.2", "reverse": true}, + {"partitionKey": "root", "prefix": "d.", "limit": 2} + ]"# + .to_vec(), + ) + .method(Method::POST) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + let json_res = json_body(res).await; + assert_json_eq!( + json_res, + json!([ + { + "partitionKey": "root", + "prefix": null, + "start": null, + "end": null, + "limit": null, + "reverse": false, + "conflictsOnly": false, + "tombstones": false, + "singleItem": false, + "items": [ + {"sk": "a", "ct": ct.get("a").unwrap(), "v": [base64::encode(values.get("a").unwrap())]}, + {"sk": "c", "ct": ct.get("c").unwrap(), "v": [base64::encode(values.get("c").unwrap()), base64::encode(values.get("c'").unwrap())]}, + {"sk": "d.1", "ct": ct.get("d.1").unwrap(), "v": [base64::encode(values.get("d.1'").unwrap())]}, + {"sk": "d.2", "ct": ct.get("d.2").unwrap(), "v": [base64::encode(values.get("d.2").unwrap()), base64::encode(values.get("d.2'").unwrap())]}, + {"sk": "e", "ct": ct.get("e").unwrap(), "v": [base64::encode(values.get("e").unwrap())]} + ], + "more": false, + "nextStart": null, + }, + { + "partitionKey": "root", + "prefix": "d", + "start": null, + "end": null, + "limit": null, + "reverse": false, + "conflictsOnly": false, + "tombstones": false, + "singleItem": false, + "items": [ + {"sk": "d.1", "ct": ct.get("d.1").unwrap(), "v": [base64::encode(values.get("d.1'").unwrap())]}, + {"sk": "d.2", "ct": ct.get("d.2").unwrap(), "v": [base64::encode(values.get("d.2").unwrap()), base64::encode(values.get("d.2'").unwrap())]}, + ], + "more": false, + "nextStart": null, + }, + { + "partitionKey": "root", + "prefix": "d.", + "start": null, + "end": "d.2", + "limit": null, + "reverse": false, + "conflictsOnly": false, + "tombstones": false, + "singleItem": false, + "items": [ + {"sk": "d.1", "ct": ct.get("d.1").unwrap(), "v": [base64::encode(values.get("d.1'").unwrap())]}, + ], + "more": false, + "nextStart": null, + }, + { + "partitionKey": "root", + "prefix": "d.", + "start": null, + "end": null, + "limit": 1, + "reverse": false, + "conflictsOnly": false, + "tombstones": false, + "singleItem": false, + "items": [ + {"sk": "d.1", "ct": ct.get("d.1").unwrap(), "v": [base64::encode(values.get("d.1'").unwrap())]}, + ], + "more": true, + "nextStart": "d.2", + }, + { + "partitionKey": "root", + "prefix": "d.", + "start": "d.2", + "end": null, + "limit": 1, + "reverse": false, + "conflictsOnly": false, + "tombstones": false, + "singleItem": false, + "items": [ + {"sk": "d.2", "ct": ct.get("d.2").unwrap(), "v": [base64::encode(values.get("d.2").unwrap()), base64::encode(values.get("d.2'").unwrap())]}, + ], + "more": false, + "nextStart": null, + }, + { + "partitionKey": "root", + "prefix": "d.", + "start": null, + "end": null, + "limit": null, + "reverse": true, + "conflictsOnly": false, + "tombstones": false, + "singleItem": false, + "items": [ + {"sk": "d.2", "ct": ct.get("d.2").unwrap(), "v": [base64::encode(values.get("d.2").unwrap()), base64::encode(values.get("d.2'").unwrap())]}, + {"sk": "d.1", "ct": ct.get("d.1").unwrap(), "v": [base64::encode(values.get("d.1'").unwrap())]}, + ], + "more": false, + "nextStart": null, + }, + { + "partitionKey": "root", + "prefix": "d.", + "start": "d.2", + "end": null, + "limit": null, + "reverse": true, + "conflictsOnly": false, + "tombstones": false, + "singleItem": false, + "items": [ + {"sk": "d.2", "ct": ct.get("d.2").unwrap(), "v": [base64::encode(values.get("d.2").unwrap()), base64::encode(values.get("d.2'").unwrap())]}, + {"sk": "d.1", "ct": ct.get("d.1").unwrap(), "v": [base64::encode(values.get("d.1'").unwrap())]}, + ], + "more": false, + "nextStart": null, + }, + { + "partitionKey": "root", + "prefix": "d.", + "start": null, + "end": null, + "limit": 2, + "reverse": false, + "conflictsOnly": false, + "tombstones": false, + "singleItem": false, + "items": [ + {"sk": "d.1", "ct": ct.get("d.1").unwrap(), "v": [base64::encode(values.get("d.1'").unwrap())]}, + {"sk": "d.2", "ct": ct.get("d.2").unwrap(), "v": [base64::encode(values.get("d.2").unwrap()), base64::encode(values.get("d.2'").unwrap())]}, + ], + "more": false, + "nextStart": null, + }, + ]) + ); + + // Test DeleteBatch + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .query_param("delete", Option::<&str>::None) + .body( + br#"[ + {"partitionKey": "root", "start": "a", "end": "c"}, + {"partitionKey": "root", "prefix": "d"} + ]"# + .to_vec(), + ) + .method(Method::POST) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + let json_res = json_body(res).await; + assert_json_eq!( + json_res, + json!([ + { + "partitionKey": "root", + "prefix": null, + "start": "a", + "end": "c", + "singleItem": false, + "deletedItems": 1, + }, + { + "partitionKey": "root", + "prefix": "d", + "start": null, + "end": null, + "singleItem": false, + "deletedItems": 2, + }, + ]) + ); + + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .query_param("search", Option::<&str>::None) + .body( + br#"[ + {"partitionKey": "root"}, + {"partitionKey": "root", "reverse": true} + ]"# + .to_vec(), + ) + .method(Method::POST) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + let json_res = json_body(res).await; + assert_json_eq!( + json_res, + json!([ + { + "partitionKey": "root", + "prefix": null, + "start": null, + "end": null, + "limit": null, + "reverse": false, + "conflictsOnly": false, + "tombstones": false, + "singleItem": false, + "items": [ + {"sk": "c", "ct": ct.get("c").unwrap(), "v": [base64::encode(values.get("c").unwrap()), base64::encode(values.get("c'").unwrap())]}, + {"sk": "e", "ct": ct.get("e").unwrap(), "v": [base64::encode(values.get("e").unwrap())]} + ], + "more": false, + "nextStart": null, + }, + { + "partitionKey": "root", + "prefix": null, + "start": null, + "end": null, + "limit": null, + "reverse": true, + "conflictsOnly": false, + "tombstones": false, + "singleItem": false, + "items": [ + {"sk": "e", "ct": ct.get("e").unwrap(), "v": [base64::encode(values.get("e").unwrap())]}, + {"sk": "c", "ct": ct.get("c").unwrap(), "v": [base64::encode(values.get("c").unwrap()), base64::encode(values.get("c'").unwrap())]}, + ], + "more": false, + "nextStart": null, + }, + ]) + ); +} diff --git a/src/garage/tests/k2v/errorcodes.rs b/src/garage/tests/k2v/errorcodes.rs new file mode 100644 index 00000000..2fcc45bc --- /dev/null +++ b/src/garage/tests/k2v/errorcodes.rs @@ -0,0 +1,141 @@ +use crate::common; + +use hyper::Method; + +#[tokio::test] +async fn test_error_codes() { + let ctx = common::context(); + let bucket = ctx.create_bucket("test-k2v-error-codes"); + + // Regular insert should work (code 200) + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .method(Method::PUT) + .path("root") + .query_param("sort_key", Some("test1")) + .body(b"Hello, world!".to_vec()) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + + // Insert with trash causality token: invalid request + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .method(Method::PUT) + .path("root") + .query_param("sort_key", Some("test1")) + .signed_header("x-garage-causality-token", "tra$sh") + .body(b"Hello, world!".to_vec()) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 400); + + // Search without partition key: invalid request + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .query_param("search", Option::<&str>::None) + .body( + br#"[ + {}, + ]"# + .to_vec(), + ) + .method(Method::POST) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 400); + + // Search with start that is not in prefix: invalid request + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .query_param("search", Option::<&str>::None) + .body( + br#"[ + {"partition_key": "root", "prefix": "a", "start": "bx"}, + ]"# + .to_vec(), + ) + .method(Method::POST) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 400); + + // Search with invalid json: 400 + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .query_param("search", Option::<&str>::None) + .body( + br#"[ + {"partition_key": "root" + ]"# + .to_vec(), + ) + .method(Method::POST) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 400); + + // Batch insert with invalid causality token: 400 + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .body( + br#"[ + {"pk": "root", "sk": "a", "ct": "tra$h", "v": "aGVsbG8sIHdvcmxkCg=="} + ]"# + .to_vec(), + ) + .method(Method::POST) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 400); + + // Batch insert with invalid data: 400 + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .body( + br#"[ + {"pk": "root", "sk": "a", "ct": null, "v": "aGVsbG8sIHdvcmx$Cg=="} + ]"# + .to_vec(), + ) + .method(Method::POST) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 400); + + // Poll with invalid causality token: 400 + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("test1")) + .query_param("causality_token", Some("tra$h")) + .query_param("timeout", Some("10")) + .signed_header("accept", "application/octet-stream") + .send() + .await + .unwrap(); + assert_eq!(res.status(), 400); +} diff --git a/src/garage/tests/k2v/item.rs b/src/garage/tests/k2v/item.rs new file mode 100644 index 00000000..bf2b01f8 --- /dev/null +++ b/src/garage/tests/k2v/item.rs @@ -0,0 +1,719 @@ +use crate::common; + +use assert_json_diff::assert_json_eq; +use serde_json::json; + +use super::json_body; +use hyper::Method; + +#[tokio::test] +async fn test_items_and_indices() { + let ctx = common::context(); + let bucket = ctx.create_bucket("test-k2v-item-and-index"); + + // ReadIndex -- there should be nothing + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .send() + .await + .unwrap(); + let res_body = json_body(res).await; + assert_json_eq!( + res_body, + json!({ + "prefix": null, + "start": null, + "end": null, + "limit": null, + "reverse": false, + "partitionKeys": [], + "more": false, + "nextStart": null + }) + ); + + let content2_len = "_: hello universe".len(); + let content3_len = "_: concurrent value".len(); + + for (i, sk) in ["a", "b", "c", "d"].iter().enumerate() { + let content = format!("{}: hello world", sk).into_bytes(); + let content2 = format!("{}: hello universe", sk).into_bytes(); + let content3 = format!("{}: concurrent value", sk).into_bytes(); + + // Put initially, no causality token + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some(sk)) + .body(content.clone()) + .method(Method::PUT) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + + // Get value back + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some(sk)) + .signed_header("accept", "*/*") + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + assert_eq!( + res.headers().get("content-type").unwrap().to_str().unwrap(), + "application/octet-stream" + ); + let ct = res + .headers() + .get("x-garage-causality-token") + .unwrap() + .to_str() + .unwrap() + .to_string(); + let res_body = hyper::body::to_bytes(res.into_body()) + .await + .unwrap() + .to_vec(); + assert_eq!(res_body, content); + + // ReadIndex -- now there should be some stuff + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .send() + .await + .unwrap(); + let res_body = json_body(res).await; + assert_json_eq!( + res_body, + json!({ + "prefix": null, + "start": null, + "end": null, + "limit": null, + "reverse": false, + "partitionKeys": [ + { + "pk": "root", + "entries": i+1, + "conflicts": i, + "values": i+i+1, + "bytes": i*(content2.len() + content3.len()) + content.len(), + } + ], + "more": false, + "nextStart": null + }) + ); + + // Put again, this time with causality token + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some(sk)) + .signed_header("x-garage-causality-token", ct.clone()) + .body(content2.clone()) + .method(Method::PUT) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + + // Get value back + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some(sk)) + .signed_header("accept", "*/*") + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + assert_eq!( + res.headers().get("content-type").unwrap().to_str().unwrap(), + "application/octet-stream" + ); + let res_body = hyper::body::to_bytes(res.into_body()) + .await + .unwrap() + .to_vec(); + assert_eq!(res_body, content2); + + // ReadIndex -- now there should be some stuff + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .send() + .await + .unwrap(); + let res_body = json_body(res).await; + assert_json_eq!( + res_body, + json!({ + "prefix": null, + "start": null, + "end": null, + "limit": null, + "reverse": false, + "partitionKeys": [ + { + "pk": "root", + "entries": i+1, + "conflicts": i, + "values": i+i+1, + "bytes": i*content3.len() + (i+1)*content2.len(), + } + ], + "more": false, + "nextStart": null + }) + ); + + // Put again with same CT, now we have concurrent values + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some(sk)) + .signed_header("x-garage-causality-token", ct.clone()) + .body(content3.clone()) + .method(Method::PUT) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + + // Get value back + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some(sk)) + .signed_header("accept", "*/*") + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + assert_eq!( + res.headers().get("content-type").unwrap().to_str().unwrap(), + "application/json" + ); + let res_json = json_body(res).await; + assert_json_eq!( + res_json, + [base64::encode(&content2), base64::encode(&content3)] + ); + + // ReadIndex -- now there should be some stuff + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .send() + .await + .unwrap(); + let res_body = json_body(res).await; + assert_json_eq!( + res_body, + json!({ + "prefix": null, + "start": null, + "end": null, + "limit": null, + "reverse": false, + "partitionKeys": [ + { + "pk": "root", + "entries": i+1, + "conflicts": i+1, + "values": 2*(i+1), + "bytes": (i+1)*(content2.len() + content3.len()), + } + ], + "more": false, + "nextStart": null + }) + ); + } + + // Now delete things + for (i, sk) in ["a", "b", "c", "d"].iter().enumerate() { + // Get value back (we just need the CT) + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some(sk)) + .signed_header("accept", "*/*") + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + let ct = res + .headers() + .get("x-garage-causality-token") + .unwrap() + .to_str() + .unwrap() + .to_string(); + + // Delete it + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .method(Method::DELETE) + .path("root") + .query_param("sort_key", Some(sk)) + .signed_header("x-garage-causality-token", ct) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 204); + + // ReadIndex -- now there should be some stuff + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .send() + .await + .unwrap(); + let res_body = json_body(res).await; + if i < 3 { + assert_json_eq!( + res_body, + json!({ + "prefix": null, + "start": null, + "end": null, + "limit": null, + "reverse": false, + "partitionKeys": [ + { + "pk": "root", + "entries": 3-i, + "conflicts": 3-i, + "values": 2*(3-i), + "bytes": (3-i)*(content2_len + content3_len), + } + ], + "more": false, + "nextStart": null + }) + ); + } else { + assert_json_eq!( + res_body, + json!({ + "prefix": null, + "start": null, + "end": null, + "limit": null, + "reverse": false, + "partitionKeys": [], + "more": false, + "nextStart": null + }) + ); + } + } +} + +#[tokio::test] +async fn test_item_return_format() { + let ctx = common::context(); + let bucket = ctx.create_bucket("test-k2v-item-return-format"); + + let single_value = b"A single value".to_vec(); + let concurrent_value = b"A concurrent value".to_vec(); + + // -- Test with a single value -- + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("v1")) + .body(single_value.clone()) + .method(Method::PUT) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + + // f0: either + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("v1")) + .signed_header("accept", "*/*") + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + assert_eq!( + res.headers().get("content-type").unwrap().to_str().unwrap(), + "application/octet-stream" + ); + let ct = res + .headers() + .get("x-garage-causality-token") + .unwrap() + .to_str() + .unwrap() + .to_string(); + let res_body = hyper::body::to_bytes(res.into_body()) + .await + .unwrap() + .to_vec(); + assert_eq!(res_body, single_value); + + // f1: not specified + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("v1")) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + assert_eq!( + res.headers().get("content-type").unwrap().to_str().unwrap(), + "application/json" + ); + let res_body = json_body(res).await; + assert_json_eq!(res_body, json!([base64::encode(&single_value)])); + + // f2: binary + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("v1")) + .signed_header("accept", "application/octet-stream") + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + assert_eq!( + res.headers().get("content-type").unwrap().to_str().unwrap(), + "application/octet-stream" + ); + let res_body = hyper::body::to_bytes(res.into_body()) + .await + .unwrap() + .to_vec(); + assert_eq!(res_body, single_value); + + // f3: json + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("v1")) + .signed_header("accept", "application/json") + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + assert_eq!( + res.headers().get("content-type").unwrap().to_str().unwrap(), + "application/json" + ); + let res_body = json_body(res).await; + assert_json_eq!(res_body, json!([base64::encode(&single_value)])); + + // -- Test with a second, concurrent value -- + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("v1")) + .body(concurrent_value.clone()) + .method(Method::PUT) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + + // f0: either + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("v1")) + .signed_header("accept", "*/*") + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + assert_eq!( + res.headers().get("content-type").unwrap().to_str().unwrap(), + "application/json" + ); + let res_body = json_body(res).await; + assert_json_eq!( + res_body, + json!([ + base64::encode(&single_value), + base64::encode(&concurrent_value) + ]) + ); + + // f1: not specified + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("v1")) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + assert_eq!( + res.headers().get("content-type").unwrap().to_str().unwrap(), + "application/json" + ); + let res_body = json_body(res).await; + assert_json_eq!( + res_body, + json!([ + base64::encode(&single_value), + base64::encode(&concurrent_value) + ]) + ); + + // f2: binary + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("v1")) + .signed_header("accept", "application/octet-stream") + .send() + .await + .unwrap(); + assert_eq!(res.status(), 409); // CONFLICT + + // f3: json + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("v1")) + .signed_header("accept", "application/json") + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + assert_eq!( + res.headers().get("content-type").unwrap().to_str().unwrap(), + "application/json" + ); + let res_body = json_body(res).await; + assert_json_eq!( + res_body, + json!([ + base64::encode(&single_value), + base64::encode(&concurrent_value) + ]) + ); + + // -- Delete first value, concurrently with second insert -- + // -- (we now have a concurrent value and a deletion) -- + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("v1")) + .method(Method::DELETE) + .signed_header("x-garage-causality-token", ct) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 204); + + // f0: either + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("v1")) + .signed_header("accept", "*/*") + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + assert_eq!( + res.headers().get("content-type").unwrap().to_str().unwrap(), + "application/json" + ); + let res_body = json_body(res).await; + assert_json_eq!(res_body, json!([base64::encode(&concurrent_value), null])); + + // f1: not specified + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("v1")) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + assert_eq!( + res.headers().get("content-type").unwrap().to_str().unwrap(), + "application/json" + ); + let ct = res + .headers() + .get("x-garage-causality-token") + .unwrap() + .to_str() + .unwrap() + .to_string(); + let res_body = json_body(res).await; + assert_json_eq!(res_body, json!([base64::encode(&concurrent_value), null])); + + // f2: binary + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("v1")) + .signed_header("accept", "application/octet-stream") + .send() + .await + .unwrap(); + assert_eq!(res.status(), 409); // CONFLICT + + // f3: json + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("v1")) + .signed_header("accept", "application/json") + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + assert_eq!( + res.headers().get("content-type").unwrap().to_str().unwrap(), + "application/json" + ); + let res_body = json_body(res).await; + assert_json_eq!(res_body, json!([base64::encode(&concurrent_value), null])); + + // -- Delete everything -- + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("v1")) + .method(Method::DELETE) + .signed_header("x-garage-causality-token", ct) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 204); + + // f0: either + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("v1")) + .signed_header("accept", "*/*") + .send() + .await + .unwrap(); + assert_eq!(res.status(), 204); // NO CONTENT + + // f1: not specified + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("v1")) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + assert_eq!( + res.headers().get("content-type").unwrap().to_str().unwrap(), + "application/json" + ); + let res_body = json_body(res).await; + assert_json_eq!(res_body, json!([null])); + + // f2: binary + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("v1")) + .signed_header("accept", "application/octet-stream") + .send() + .await + .unwrap(); + assert_eq!(res.status(), 204); // NO CONTENT + + // f3: json + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("v1")) + .signed_header("accept", "application/json") + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + assert_eq!( + res.headers().get("content-type").unwrap().to_str().unwrap(), + "application/json" + ); + let res_body = json_body(res).await; + assert_json_eq!(res_body, json!([null])); +} diff --git a/src/garage/tests/k2v/mod.rs b/src/garage/tests/k2v/mod.rs new file mode 100644 index 00000000..a009460e --- /dev/null +++ b/src/garage/tests/k2v/mod.rs @@ -0,0 +1,18 @@ +pub mod batch; +pub mod errorcodes; +pub mod item; +pub mod poll; +pub mod simple; + +use hyper::{Body, Response}; + +pub async fn json_body(res: Response) -> serde_json::Value { + let res_body: serde_json::Value = serde_json::from_slice( + &hyper::body::to_bytes(res.into_body()) + .await + .unwrap() + .to_vec()[..], + ) + .unwrap(); + res_body +} diff --git a/src/garage/tests/k2v/poll.rs b/src/garage/tests/k2v/poll.rs new file mode 100644 index 00000000..70dc0410 --- /dev/null +++ b/src/garage/tests/k2v/poll.rs @@ -0,0 +1,98 @@ +use hyper::Method; +use std::time::Duration; + +use crate::common; + +#[tokio::test] +async fn test_poll() { + let ctx = common::context(); + let bucket = ctx.create_bucket("test-k2v-poll"); + + // Write initial value + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .method(Method::PUT) + .path("root") + .query_param("sort_key", Some("test1")) + .body(b"Initial value".to_vec()) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + + // Retrieve initial value to get its causality token + let res2 = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("test1")) + .signed_header("accept", "application/octet-stream") + .send() + .await + .unwrap(); + assert_eq!(res2.status(), 200); + let ct = res2 + .headers() + .get("x-garage-causality-token") + .unwrap() + .to_str() + .unwrap() + .to_string(); + + let res2_body = hyper::body::to_bytes(res2.into_body()) + .await + .unwrap() + .to_vec(); + assert_eq!(res2_body, b"Initial value"); + + // Start poll operation + let poll = { + let bucket = bucket.clone(); + let ct = ct.clone(); + tokio::spawn(async move { + let ctx = common::context(); + ctx.k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("test1")) + .query_param("causality_token", Some(ct)) + .query_param("timeout", Some("10")) + .signed_header("accept", "application/octet-stream") + .send() + .await + }) + }; + + // Write new value that supersedes initial one + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .method(Method::PUT) + .path("root") + .query_param("sort_key", Some("test1")) + .signed_header("x-garage-causality-token", ct) + .body(b"New value".to_vec()) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + + // Check poll finishes with correct value + let poll_res = tokio::select! { + _ = tokio::time::sleep(Duration::from_secs(10)) => panic!("poll did not terminate in time"), + res = poll => res.unwrap().unwrap(), + }; + + assert_eq!(poll_res.status(), 200); + + let poll_res_body = hyper::body::to_bytes(poll_res.into_body()) + .await + .unwrap() + .to_vec(); + assert_eq!(poll_res_body, b"New value"); +} diff --git a/src/garage/tests/k2v/simple.rs b/src/garage/tests/k2v/simple.rs new file mode 100644 index 00000000..ae9a8674 --- /dev/null +++ b/src/garage/tests/k2v/simple.rs @@ -0,0 +1,40 @@ +use crate::common; + +use hyper::Method; + +#[tokio::test] +async fn test_simple() { + let ctx = common::context(); + let bucket = ctx.create_bucket("test-k2v-simple"); + + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .method(Method::PUT) + .path("root") + .query_param("sort_key", Some("test1")) + .body(b"Hello, world!".to_vec()) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + + let res2 = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some("test1")) + .signed_header("accept", "application/octet-stream") + .send() + .await + .unwrap(); + assert_eq!(res2.status(), 200); + + let res2_body = hyper::body::to_bytes(res2.into_body()) + .await + .unwrap() + .to_vec(); + assert_eq!(res2_body, b"Hello, world!"); +} diff --git a/src/garage/tests/lib.rs b/src/garage/tests/lib.rs index 8799c395..0106ad10 100644 --- a/src/garage/tests/lib.rs +++ b/src/garage/tests/lib.rs @@ -3,9 +3,5 @@ mod common; mod admin; mod bucket; -mod list; -mod multipart; -mod objects; -mod simple; -mod streaming_signature; -mod website; +mod k2v; +mod s3; diff --git a/src/garage/tests/list.rs b/src/garage/tests/s3/list.rs similarity index 100% rename from src/garage/tests/list.rs rename to src/garage/tests/s3/list.rs diff --git a/src/garage/tests/s3/mod.rs b/src/garage/tests/s3/mod.rs new file mode 100644 index 00000000..623eb665 --- /dev/null +++ b/src/garage/tests/s3/mod.rs @@ -0,0 +1,6 @@ +mod list; +mod multipart; +mod objects; +mod simple; +mod streaming_signature; +mod website; diff --git a/src/garage/tests/multipart.rs b/src/garage/tests/s3/multipart.rs similarity index 100% rename from src/garage/tests/multipart.rs rename to src/garage/tests/s3/multipart.rs diff --git a/src/garage/tests/objects.rs b/src/garage/tests/s3/objects.rs similarity index 100% rename from src/garage/tests/objects.rs rename to src/garage/tests/s3/objects.rs diff --git a/src/garage/tests/simple.rs b/src/garage/tests/s3/simple.rs similarity index 100% rename from src/garage/tests/simple.rs rename to src/garage/tests/s3/simple.rs diff --git a/src/garage/tests/streaming_signature.rs b/src/garage/tests/s3/streaming_signature.rs similarity index 100% rename from src/garage/tests/streaming_signature.rs rename to src/garage/tests/s3/streaming_signature.rs diff --git a/src/garage/tests/website.rs b/src/garage/tests/s3/website.rs similarity index 92% rename from src/garage/tests/website.rs rename to src/garage/tests/s3/website.rs index 963d11ea..0570ac6a 100644 --- a/src/garage/tests/website.rs +++ b/src/garage/tests/s3/website.rs @@ -35,10 +35,7 @@ async fn test_website() { let req = || { Request::builder() .method("GET") - .uri(format!( - "http://127.0.0.1:{}/", - common::garage::DEFAULT_PORT + 2 - )) + .uri(format!("http://127.0.0.1:{}/", ctx.garage.web_port)) .header("Host", format!("{}.web.garage", BCKT_NAME)) .body(Body::empty()) .unwrap() @@ -170,10 +167,7 @@ async fn test_website_s3_api() { { let req = Request::builder() .method("GET") - .uri(format!( - "http://127.0.0.1:{}/site/", - common::garage::DEFAULT_PORT + 2 - )) + .uri(format!("http://127.0.0.1:{}/site/", ctx.garage.web_port)) .header("Host", format!("{}.web.garage", BCKT_NAME)) .header("Origin", "https://example.com") .body(Body::empty()) @@ -198,7 +192,7 @@ async fn test_website_s3_api() { .method("GET") .uri(format!( "http://127.0.0.1:{}/wrong.html", - common::garage::DEFAULT_PORT + 2 + ctx.garage.web_port )) .header("Host", format!("{}.web.garage", BCKT_NAME)) .body(Body::empty()) @@ -217,10 +211,7 @@ async fn test_website_s3_api() { { let req = Request::builder() .method("OPTIONS") - .uri(format!( - "http://127.0.0.1:{}/site/", - common::garage::DEFAULT_PORT + 2 - )) + .uri(format!("http://127.0.0.1:{}/site/", ctx.garage.web_port)) .header("Host", format!("{}.web.garage", BCKT_NAME)) .header("Origin", "https://example.com") .header("Access-Control-Request-Method", "PUT") @@ -244,10 +235,7 @@ async fn test_website_s3_api() { { let req = Request::builder() .method("OPTIONS") - .uri(format!( - "http://127.0.0.1:{}/site/", - common::garage::DEFAULT_PORT + 2 - )) + .uri(format!("http://127.0.0.1:{}/site/", ctx.garage.web_port)) .header("Host", format!("{}.web.garage", BCKT_NAME)) .header("Origin", "https://example.com") .header("Access-Control-Request-Method", "DELETE") @@ -288,10 +276,7 @@ async fn test_website_s3_api() { { let req = Request::builder() .method("OPTIONS") - .uri(format!( - "http://127.0.0.1:{}/site/", - common::garage::DEFAULT_PORT + 2 - )) + .uri(format!("http://127.0.0.1:{}/site/", ctx.garage.web_port)) .header("Host", format!("{}.web.garage", BCKT_NAME)) .header("Origin", "https://example.com") .header("Access-Control-Request-Method", "PUT") @@ -319,10 +304,7 @@ async fn test_website_s3_api() { { let req = Request::builder() .method("GET") - .uri(format!( - "http://127.0.0.1:{}/site/", - common::garage::DEFAULT_PORT + 2 - )) + .uri(format!("http://127.0.0.1:{}/site/", ctx.garage.web_port)) .header("Host", format!("{}.web.garage", BCKT_NAME)) .body(Body::empty()) .unwrap(); diff --git a/src/model/Cargo.toml b/src/model/Cargo.toml index 007cec89..133fe44e 100644 --- a/src/model/Cargo.toml +++ b/src/model/Cargo.toml @@ -22,8 +22,10 @@ garage_model_050 = { package = "garage_model", version = "0.5.1" } async-trait = "0.1.7" arc-swap = "1.0" +blake2 = "0.9" err-derive = "0.3" hex = "0.4" +base64 = "0.13" tracing = "0.1.30" rand = "0.8" zstd = { version = "0.9", default-features = false } @@ -42,3 +44,6 @@ opentelemetry = "0.17" #netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" } #netapp = { version = "0.4", path = "../../../netapp" } netapp = "0.4" + +[features] +k2v = [ "garage_util/k2v" ] diff --git a/src/model/garage.rs b/src/model/garage.rs index abdb920a..03e21f8a 100644 --- a/src/model/garage.rs +++ b/src/model/garage.rs @@ -13,13 +13,19 @@ use garage_table::replication::TableFullReplication; use garage_table::replication::TableShardedReplication; use garage_table::*; -use crate::block_ref_table::*; +use crate::s3::block_ref_table::*; +use crate::s3::object_table::*; +use crate::s3::version_table::*; + use crate::bucket_alias_table::*; use crate::bucket_table::*; use crate::helper; use crate::key_table::*; -use crate::object_table::*; -use crate::version_table::*; + +#[cfg(feature = "k2v")] +use crate::index_counter::*; +#[cfg(feature = "k2v")] +use crate::k2v::{counter_table::*, item_table::*, poll::*, rpc::*}; /// An entire Garage full of data pub struct Garage { @@ -35,16 +41,32 @@ pub struct Garage { /// The block manager pub block_manager: Arc, - /// Table containing informations about buckets + /// Table containing buckets pub bucket_table: Arc>, - /// Table containing informations about bucket aliases + /// Table containing bucket aliases pub bucket_alias_table: Arc>, - /// Table containing informations about api keys + /// Table containing api keys pub key_table: Arc>, + /// Table containing S3 objects pub object_table: Arc>, + /// Table containing S3 object versions pub version_table: Arc>, + /// Table containing S3 block references (not blocks themselves) pub block_ref_table: Arc>, + + #[cfg(feature = "k2v")] + pub k2v: GarageK2V, +} + +#[cfg(feature = "k2v")] +pub struct GarageK2V { + /// Table containing K2V items + pub item_table: Arc>, + /// Indexing table containing K2V item counters + pub counter_table: Arc>, + /// K2V RPC handler + pub rpc: Arc, } impl Garage { @@ -95,6 +117,21 @@ impl Garage { system.clone(), ); + // ---- admin tables ---- + info!("Initialize bucket_table..."); + let bucket_table = Table::new(BucketTable, control_rep_param.clone(), system.clone(), &db); + + info!("Initialize bucket_alias_table..."); + let bucket_alias_table = Table::new( + BucketAliasTable, + control_rep_param.clone(), + system.clone(), + &db, + ); + info!("Initialize key_table_table..."); + let key_table = Table::new(KeyTable, control_rep_param, system.clone(), &db); + + // ---- S3 tables ---- info!("Initialize block_ref_table..."); let block_ref_table = Table::new( BlockRefTable { @@ -117,29 +154,20 @@ impl Garage { ); info!("Initialize object_table..."); + #[allow(clippy::redundant_clone)] let object_table = Table::new( ObjectTable { background: background.clone(), version_table: version_table.clone(), }, - meta_rep_param, + meta_rep_param.clone(), system.clone(), &db, ); - info!("Initialize bucket_table..."); - let bucket_table = Table::new(BucketTable, control_rep_param.clone(), system.clone(), &db); - - info!("Initialize bucket_alias_table..."); - let bucket_alias_table = Table::new( - BucketAliasTable, - control_rep_param.clone(), - system.clone(), - &db, - ); - - info!("Initialize key_table_table..."); - let key_table = Table::new(KeyTable, control_rep_param, system.clone(), &db); + // ---- K2V ---- + #[cfg(feature = "k2v")] + let k2v = GarageK2V::new(system.clone(), &db, meta_rep_param); info!("Initialize Garage..."); @@ -155,6 +183,8 @@ impl Garage { object_table, version_table, block_ref_table, + #[cfg(feature = "k2v")] + k2v, }) } @@ -162,3 +192,30 @@ impl Garage { helper::bucket::BucketHelper(self) } } + +#[cfg(feature = "k2v")] +impl GarageK2V { + fn new(system: Arc, db: &sled::Db, meta_rep_param: TableShardedReplication) -> Self { + info!("Initialize K2V counter table..."); + let counter_table = IndexCounter::new(system.clone(), meta_rep_param.clone(), db); + info!("Initialize K2V subscription manager..."); + let subscriptions = Arc::new(SubscriptionManager::new()); + info!("Initialize K2V item table..."); + let item_table = Table::new( + K2VItemTable { + counter_table: counter_table.clone(), + subscriptions: subscriptions.clone(), + }, + meta_rep_param, + system.clone(), + db, + ); + let rpc = K2VRpcHandler::new(system, item_table.clone(), subscriptions); + + Self { + item_table, + counter_table, + rpc, + } + } +} diff --git a/src/model/helper/bucket.rs b/src/model/helper/bucket.rs index 706faf26..54d2f97b 100644 --- a/src/model/helper/bucket.rs +++ b/src/model/helper/bucket.rs @@ -1,4 +1,4 @@ -use garage_table::util::EmptyKey; +use garage_table::util::*; use garage_util::crdt::*; use garage_util::data::*; use garage_util::error::{Error as GarageError, OkOrMessage}; @@ -116,6 +116,7 @@ impl<'a> BucketHelper<'a> { None, Some(KeyFilter::MatchesAndNotDeleted(pattern.to_string())), 10, + EnumerationOrder::Forward, ) .await? .into_iter() diff --git a/src/model/index_counter.rs b/src/model/index_counter.rs new file mode 100644 index 00000000..123154d4 --- /dev/null +++ b/src/model/index_counter.rs @@ -0,0 +1,305 @@ +use std::collections::{hash_map, BTreeMap, HashMap}; +use std::marker::PhantomData; +use std::sync::Arc; +use std::time::Duration; + +use serde::{Deserialize, Serialize}; +use tokio::sync::{mpsc, watch}; + +use garage_rpc::ring::Ring; +use garage_rpc::system::System; +use garage_util::data::*; +use garage_util::error::*; + +use garage_table::crdt::*; +use garage_table::replication::TableShardedReplication; +use garage_table::*; + +pub trait CounterSchema: Clone + PartialEq + Send + Sync + 'static { + const NAME: &'static str; + type P: PartitionKey + Clone + PartialEq + Serialize + for<'de> Deserialize<'de> + Send + Sync; + type S: SortKey + Clone + PartialEq + Serialize + for<'de> Deserialize<'de> + Send + Sync; +} + +/// A counter entry in the global table +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +pub struct CounterEntry { + pub pk: T::P, + pub sk: T::S, + pub values: BTreeMap, +} + +impl Entry for CounterEntry { + fn partition_key(&self) -> &T::P { + &self.pk + } + fn sort_key(&self) -> &T::S { + &self.sk + } + fn is_tombstone(&self) -> bool { + self.values + .iter() + .all(|(_, v)| v.node_values.iter().all(|(_, (_, v))| *v == 0)) + } +} + +impl CounterEntry { + pub fn filtered_values(&self, ring: &Ring) -> HashMap { + let nodes = &ring.layout.node_id_vec[..]; + self.filtered_values_with_nodes(nodes) + } + + pub fn filtered_values_with_nodes(&self, nodes: &[Uuid]) -> HashMap { + let mut ret = HashMap::new(); + for (name, vals) in self.values.iter() { + let new_vals = vals + .node_values + .iter() + .filter(|(n, _)| nodes.contains(n)) + .map(|(_, (_, v))| *v) + .collect::>(); + if !new_vals.is_empty() { + ret.insert( + name.clone(), + new_vals.iter().fold(i64::MIN, |a, b| std::cmp::max(a, *b)), + ); + } + } + + ret + } +} + +/// A counter entry in the global table +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +pub struct CounterValue { + pub node_values: BTreeMap, +} + +impl Crdt for CounterEntry { + fn merge(&mut self, other: &Self) { + for (name, e2) in other.values.iter() { + if let Some(e) = self.values.get_mut(name) { + e.merge(e2); + } else { + self.values.insert(name.clone(), e2.clone()); + } + } + } +} + +impl Crdt for CounterValue { + fn merge(&mut self, other: &Self) { + for (node, (t2, e2)) in other.node_values.iter() { + if let Some((t, e)) = self.node_values.get_mut(node) { + if t2 > t { + *e = *e2; + } + } else { + self.node_values.insert(*node, (*t2, *e2)); + } + } + } +} + +pub struct CounterTable { + _phantom_t: PhantomData, +} + +impl TableSchema for CounterTable { + const TABLE_NAME: &'static str = T::NAME; + + type P = T::P; + type S = T::S; + type E = CounterEntry; + type Filter = (DeletedFilter, Vec); + + fn updated(&self, _old: Option<&Self::E>, _new: Option<&Self::E>) { + // nothing for now + } + + fn matches_filter(entry: &Self::E, filter: &Self::Filter) -> bool { + if filter.0 == DeletedFilter::Any { + return true; + } + + let is_tombstone = entry + .filtered_values_with_nodes(&filter.1[..]) + .iter() + .all(|(_, v)| *v == 0); + filter.0.apply(is_tombstone) + } +} + +// ---- + +pub struct IndexCounter { + this_node: Uuid, + local_counter: sled::Tree, + propagate_tx: mpsc::UnboundedSender<(T::P, T::S, LocalCounterEntry)>, + pub table: Arc, TableShardedReplication>>, +} + +impl IndexCounter { + pub fn new( + system: Arc, + replication: TableShardedReplication, + db: &sled::Db, + ) -> Arc { + let background = system.background.clone(); + + let (propagate_tx, propagate_rx) = mpsc::unbounded_channel(); + + let this = Arc::new(Self { + this_node: system.id, + local_counter: db + .open_tree(format!("local_counter:{}", T::NAME)) + .expect("Unable to open local counter tree"), + propagate_tx, + table: Table::new( + CounterTable { + _phantom_t: Default::default(), + }, + replication, + system, + db, + ), + }); + + let this2 = this.clone(); + background.spawn_worker( + format!("{} index counter propagator", T::NAME), + move |must_exit| this2.clone().propagate_loop(propagate_rx, must_exit), + ); + this + } + + pub fn count(&self, pk: &T::P, sk: &T::S, counts: &[(&str, i64)]) -> Result<(), Error> { + let tree_key = self.table.data.tree_key(pk, sk); + + let new_entry = self.local_counter.transaction(|tx| { + let mut entry = match tx.get(&tree_key[..])? { + Some(old_bytes) => { + rmp_serde::decode::from_read_ref::<_, LocalCounterEntry>(&old_bytes) + .map_err(Error::RmpDecode) + .map_err(sled::transaction::ConflictableTransactionError::Abort)? + } + None => LocalCounterEntry { + values: BTreeMap::new(), + }, + }; + + for (s, inc) in counts.iter() { + let mut ent = entry.values.entry(s.to_string()).or_insert((0, 0)); + ent.0 += 1; + ent.1 += *inc; + } + + let new_entry_bytes = rmp_to_vec_all_named(&entry) + .map_err(Error::RmpEncode) + .map_err(sled::transaction::ConflictableTransactionError::Abort)?; + tx.insert(&tree_key[..], new_entry_bytes)?; + + Ok(entry) + })?; + + if let Err(e) = self.propagate_tx.send((pk.clone(), sk.clone(), new_entry)) { + error!( + "Could not propagate updated counter values, failed to send to channel: {}", + e + ); + } + + Ok(()) + } + + async fn propagate_loop( + self: Arc, + mut propagate_rx: mpsc::UnboundedReceiver<(T::P, T::S, LocalCounterEntry)>, + must_exit: watch::Receiver, + ) { + // This loop batches updates to counters to be sent all at once. + // They are sent once the propagate_rx channel has been emptied (or is closed). + let mut buf = HashMap::new(); + let mut errors = 0; + + loop { + let (ent, closed) = match propagate_rx.try_recv() { + Ok(ent) => (Some(ent), false), + Err(mpsc::error::TryRecvError::Empty) if buf.is_empty() => { + match propagate_rx.recv().await { + Some(ent) => (Some(ent), false), + None => (None, true), + } + } + Err(mpsc::error::TryRecvError::Empty) => (None, false), + Err(mpsc::error::TryRecvError::Disconnected) => (None, true), + }; + + if let Some((pk, sk, counters)) = ent { + let tree_key = self.table.data.tree_key(&pk, &sk); + let dist_entry = counters.into_counter_entry::(self.this_node, pk, sk); + match buf.entry(tree_key) { + hash_map::Entry::Vacant(e) => { + e.insert(dist_entry); + } + hash_map::Entry::Occupied(mut e) => { + e.get_mut().merge(&dist_entry); + } + } + // As long as we can add entries, loop back and add them to batch + // before sending batch to other nodes + continue; + } + + if !buf.is_empty() { + let entries = buf.iter().map(|(_k, v)| v); + if let Err(e) = self.table.insert_many(entries).await { + errors += 1; + if errors >= 2 && *must_exit.borrow() { + error!("({}) Could not propagate {} counter values: {}, these counters will not be updated correctly.", T::NAME, buf.len(), e); + break; + } + warn!("({}) Could not propagate {} counter values: {}, retrying in 5 seconds (retry #{})", T::NAME, buf.len(), e, errors); + tokio::time::sleep(Duration::from_secs(5)).await; + continue; + } + + buf.clear(); + errors = 0; + } + + if closed || *must_exit.borrow() { + break; + } + } + } +} + +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +struct LocalCounterEntry { + values: BTreeMap, +} + +impl LocalCounterEntry { + fn into_counter_entry( + self, + this_node: Uuid, + pk: T::P, + sk: T::S, + ) -> CounterEntry { + CounterEntry { + pk, + sk, + values: self + .values + .into_iter() + .map(|(name, (ts, v))| { + let mut node_values = BTreeMap::new(); + node_values.insert(this_node, (ts, v)); + (name, CounterValue { node_values }) + }) + .collect(), + } + } +} diff --git a/src/model/k2v/causality.rs b/src/model/k2v/causality.rs new file mode 100644 index 00000000..8c76a32b --- /dev/null +++ b/src/model/k2v/causality.rs @@ -0,0 +1,96 @@ +use std::collections::BTreeMap; +use std::convert::TryInto; + +use serde::{Deserialize, Serialize}; + +use garage_util::data::*; + +/// Node IDs used in K2V are u64 integers that are the abbreviation +/// of full Garage node IDs which are 256-bit UUIDs. +pub type K2VNodeId = u64; + +pub fn make_node_id(node_id: Uuid) -> K2VNodeId { + let mut tmp = [0u8; 8]; + tmp.copy_from_slice(&node_id.as_slice()[..8]); + u64::from_be_bytes(tmp) +} + +#[derive(PartialEq, Debug, Serialize, Deserialize)] +pub struct CausalContext { + pub vector_clock: BTreeMap, +} + +impl CausalContext { + /// Empty causality context + pub fn new_empty() -> Self { + Self { + vector_clock: BTreeMap::new(), + } + } + /// Make binary representation and encode in base64 + pub fn serialize(&self) -> String { + let mut ints = Vec::with_capacity(2 * self.vector_clock.len()); + for (node, time) in self.vector_clock.iter() { + ints.push(*node); + ints.push(*time); + } + let checksum = ints.iter().fold(0, |acc, v| acc ^ *v); + + let mut bytes = u64::to_be_bytes(checksum).to_vec(); + for i in ints { + bytes.extend(u64::to_be_bytes(i)); + } + + base64::encode_config(bytes, base64::URL_SAFE_NO_PAD) + } + /// Parse from base64-encoded binary representation + pub fn parse(s: &str) -> Result { + let bytes = base64::decode_config(s, base64::URL_SAFE_NO_PAD) + .map_err(|e| format!("bad causality token base64: {}", e))?; + if bytes.len() % 16 != 8 || bytes.len() < 8 { + return Err("bad causality token length".into()); + } + + let checksum = u64::from_be_bytes(bytes[..8].try_into().unwrap()); + let mut ret = CausalContext { + vector_clock: BTreeMap::new(), + }; + + for i in 0..(bytes.len() / 16) { + let node_id = u64::from_be_bytes(bytes[8 + i * 16..16 + i * 16].try_into().unwrap()); + let time = u64::from_be_bytes(bytes[16 + i * 16..24 + i * 16].try_into().unwrap()); + ret.vector_clock.insert(node_id, time); + } + + let check = ret.vector_clock.iter().fold(0, |acc, (n, t)| acc ^ *n ^ *t); + + if check != checksum { + return Err("bad causality token checksum".into()); + } + + Ok(ret) + } + /// Check if this causal context contains newer items than another one + pub fn is_newer_than(&self, other: &Self) -> bool { + self.vector_clock + .iter() + .any(|(k, v)| v > other.vector_clock.get(k).unwrap_or(&0)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_causality_token_serialization() { + let ct = CausalContext { + vector_clock: [(4, 42), (1928131023, 76), (0xefc0c1c47f9de433, 2)] + .iter() + .cloned() + .collect(), + }; + + assert_eq!(CausalContext::parse(&ct.serialize()).unwrap(), ct); + } +} diff --git a/src/model/k2v/counter_table.rs b/src/model/k2v/counter_table.rs new file mode 100644 index 00000000..4856eb2b --- /dev/null +++ b/src/model/k2v/counter_table.rs @@ -0,0 +1,20 @@ +use garage_util::data::*; + +use crate::index_counter::*; + +pub const ENTRIES: &str = "entries"; +pub const CONFLICTS: &str = "conflicts"; +pub const VALUES: &str = "values"; +pub const BYTES: &str = "bytes"; + +#[derive(PartialEq, Clone)] +pub struct K2VCounterTable; + +impl CounterSchema for K2VCounterTable { + const NAME: &'static str = "k2v_index_counter"; + + // Partition key = bucket id + type P = Uuid; + // Sort key = K2V item's partition key + type S = String; +} diff --git a/src/model/k2v/item_table.rs b/src/model/k2v/item_table.rs new file mode 100644 index 00000000..8b7cc08a --- /dev/null +++ b/src/model/k2v/item_table.rs @@ -0,0 +1,291 @@ +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::sync::Arc; + +use garage_util::data::*; + +use garage_table::crdt::*; +use garage_table::*; + +use crate::index_counter::*; +use crate::k2v::causality::*; +use crate::k2v::counter_table::*; +use crate::k2v::poll::*; + +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +pub struct K2VItem { + pub partition: K2VItemPartition, + pub sort_key: String, + + items: BTreeMap, +} + +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash, Eq)] +pub struct K2VItemPartition { + pub bucket_id: Uuid, + pub partition_key: String, +} + +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +struct DvvsEntry { + t_discard: u64, + values: Vec<(u64, DvvsValue)>, +} + +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +pub enum DvvsValue { + Value(#[serde(with = "serde_bytes")] Vec), + Deleted, +} + +impl K2VItem { + /// Creates a new K2VItem when no previous entry existed in the db + pub fn new(bucket_id: Uuid, partition_key: String, sort_key: String) -> Self { + Self { + partition: K2VItemPartition { + bucket_id, + partition_key, + }, + sort_key, + items: BTreeMap::new(), + } + } + /// Updates a K2VItem with a new value or a deletion event + pub fn update( + &mut self, + this_node: Uuid, + context: &Option, + new_value: DvvsValue, + ) { + if let Some(context) = context { + for (node, t_discard) in context.vector_clock.iter() { + if let Some(e) = self.items.get_mut(node) { + e.t_discard = std::cmp::max(e.t_discard, *t_discard); + } else { + self.items.insert( + *node, + DvvsEntry { + t_discard: *t_discard, + values: vec![], + }, + ); + } + } + } + + self.discard(); + + let node_id = make_node_id(this_node); + let e = self.items.entry(node_id).or_insert(DvvsEntry { + t_discard: 0, + values: vec![], + }); + let t_prev = e.max_time(); + e.values.push((t_prev + 1, new_value)); + } + + /// Extract the causality context of a K2V Item + pub fn causal_context(&self) -> CausalContext { + let mut cc = CausalContext::new_empty(); + for (node, ent) in self.items.iter() { + cc.vector_clock.insert(*node, ent.max_time()); + } + cc + } + + /// Extract the list of values + pub fn values(&'_ self) -> Vec<&'_ DvvsValue> { + let mut ret = vec![]; + for (_, ent) in self.items.iter() { + for (_, v) in ent.values.iter() { + if !ret.contains(&v) { + ret.push(v); + } + } + } + ret + } + + fn discard(&mut self) { + for (_, ent) in self.items.iter_mut() { + ent.discard(); + } + } + + // returns counters: (non-deleted entries, conflict entries, non-tombstone values, bytes used) + fn stats(&self) -> (i64, i64, i64, i64) { + let values = self.values(); + + let n_entries = if self.is_tombstone() { 0 } else { 1 }; + let n_conflicts = if values.len() > 1 { 1 } else { 0 }; + let n_values = values + .iter() + .filter(|v| matches!(v, DvvsValue::Value(_))) + .count() as i64; + let n_bytes = values + .iter() + .map(|v| match v { + DvvsValue::Deleted => 0, + DvvsValue::Value(v) => v.len() as i64, + }) + .sum(); + + (n_entries, n_conflicts, n_values, n_bytes) + } +} + +impl DvvsEntry { + fn max_time(&self) -> u64 { + self.values + .iter() + .fold(self.t_discard, |acc, (vts, _)| std::cmp::max(acc, *vts)) + } + + fn discard(&mut self) { + self.values = std::mem::take(&mut self.values) + .into_iter() + .filter(|(t, _)| *t > self.t_discard) + .collect::>(); + } +} + +impl Crdt for K2VItem { + fn merge(&mut self, other: &Self) { + for (node, e2) in other.items.iter() { + if let Some(e) = self.items.get_mut(node) { + e.merge(e2); + } else { + self.items.insert(*node, e2.clone()); + } + } + } +} + +impl Crdt for DvvsEntry { + fn merge(&mut self, other: &Self) { + self.t_discard = std::cmp::max(self.t_discard, other.t_discard); + self.discard(); + + let t_max = self.max_time(); + for (vt, vv) in other.values.iter() { + if *vt > t_max { + self.values.push((*vt, vv.clone())); + } + } + } +} + +impl PartitionKey for K2VItemPartition { + fn hash(&self) -> Hash { + use blake2::{Blake2b, Digest}; + + let mut hasher = Blake2b::new(); + hasher.update(self.bucket_id.as_slice()); + hasher.update(self.partition_key.as_bytes()); + let mut hash = [0u8; 32]; + hash.copy_from_slice(&hasher.finalize()[..32]); + hash.into() + } +} + +impl Entry for K2VItem { + fn partition_key(&self) -> &K2VItemPartition { + &self.partition + } + fn sort_key(&self) -> &String { + &self.sort_key + } + fn is_tombstone(&self) -> bool { + self.values() + .iter() + .all(|v| matches!(v, DvvsValue::Deleted)) + } +} + +pub struct K2VItemTable { + pub(crate) counter_table: Arc>, + pub(crate) subscriptions: Arc, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub struct ItemFilter { + pub exclude_only_tombstones: bool, + pub conflicts_only: bool, +} + +impl TableSchema for K2VItemTable { + const TABLE_NAME: &'static str = "k2v_item"; + + type P = K2VItemPartition; + type S = String; + type E = K2VItem; + type Filter = ItemFilter; + + fn updated(&self, old: Option<&Self::E>, new: Option<&Self::E>) { + // 1. Count + let (old_entries, old_conflicts, old_values, old_bytes) = match old { + None => (0, 0, 0, 0), + Some(e) => e.stats(), + }; + let (new_entries, new_conflicts, new_values, new_bytes) = match new { + None => (0, 0, 0, 0), + Some(e) => e.stats(), + }; + + let count_pk = old + .map(|e| e.partition.bucket_id) + .unwrap_or_else(|| new.unwrap().partition.bucket_id); + let count_sk = old + .map(|e| &e.partition.partition_key) + .unwrap_or_else(|| &new.unwrap().partition.partition_key); + + if let Err(e) = self.counter_table.count( + &count_pk, + count_sk, + &[ + (ENTRIES, new_entries - old_entries), + (CONFLICTS, new_conflicts - old_conflicts), + (VALUES, new_values - old_values), + (BYTES, new_bytes - old_bytes), + ], + ) { + error!("Could not update K2V counter for bucket {:?} partition {}; counts will now be inconsistent. {}", count_pk, count_sk, e); + } + + // 2. Notify + if let Some(new_ent) = new { + self.subscriptions.notify(new_ent); + } + } + + #[allow(clippy::nonminimal_bool)] + fn matches_filter(entry: &Self::E, filter: &Self::Filter) -> bool { + let v = entry.values(); + !(filter.conflicts_only && v.len() < 2) + && !(filter.exclude_only_tombstones && entry.is_tombstone()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_dvvsentry_merge_simple() { + let e1 = DvvsEntry { + t_discard: 4, + values: vec![ + (5, DvvsValue::Value(vec![15])), + (6, DvvsValue::Value(vec![16])), + ], + }; + let e2 = DvvsEntry { + t_discard: 5, + values: vec![(6, DvvsValue::Value(vec![16])), (7, DvvsValue::Deleted)], + }; + + let mut e3 = e1.clone(); + e3.merge(&e2); + assert_eq!(e2, e3); + } +} diff --git a/src/model/k2v/mod.rs b/src/model/k2v/mod.rs new file mode 100644 index 00000000..664172a6 --- /dev/null +++ b/src/model/k2v/mod.rs @@ -0,0 +1,7 @@ +pub mod causality; + +pub mod counter_table; +pub mod item_table; + +pub mod poll; +pub mod rpc; diff --git a/src/model/k2v/poll.rs b/src/model/k2v/poll.rs new file mode 100644 index 00000000..93105207 --- /dev/null +++ b/src/model/k2v/poll.rs @@ -0,0 +1,50 @@ +use std::collections::HashMap; +use std::sync::Mutex; + +use serde::{Deserialize, Serialize}; +use tokio::sync::broadcast; + +use crate::k2v::item_table::*; + +#[derive(Debug, Hash, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PollKey { + pub partition: K2VItemPartition, + pub sort_key: String, +} + +#[derive(Default)] +pub struct SubscriptionManager { + subscriptions: Mutex>>, +} + +impl SubscriptionManager { + pub fn new() -> Self { + Self::default() + } + + pub fn subscribe(&self, key: &PollKey) -> broadcast::Receiver { + let mut subs = self.subscriptions.lock().unwrap(); + if let Some(s) = subs.get(key) { + s.subscribe() + } else { + let (tx, rx) = broadcast::channel(8); + subs.insert(key.clone(), tx); + rx + } + } + + pub fn notify(&self, item: &K2VItem) { + let key = PollKey { + partition: item.partition.clone(), + sort_key: item.sort_key.clone(), + }; + let mut subs = self.subscriptions.lock().unwrap(); + if let Some(s) = subs.get(&key) { + if s.send(item.clone()).is_err() { + // no more subscribers, remove channel from here + // (we will re-create it later if we need to subscribe again) + subs.remove(&key); + } + } + } +} diff --git a/src/model/k2v/rpc.rs b/src/model/k2v/rpc.rs new file mode 100644 index 00000000..90101d0f --- /dev/null +++ b/src/model/k2v/rpc.rs @@ -0,0 +1,343 @@ +//! Module that implements RPCs specific to K2V. +//! This is necessary for insertions into the K2V store, +//! as they have to be transmitted to one of the nodes responsible +//! for storing the entry to be processed (the API entry +//! node does not process the entry directly, as this would +//! mean the vector clock gets much larger than needed). + +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; + +use async_trait::async_trait; +use futures::stream::FuturesUnordered; +use futures::StreamExt; +use serde::{Deserialize, Serialize}; +use tokio::select; + +use garage_util::crdt::*; +use garage_util::data::*; +use garage_util::error::*; + +use garage_rpc::system::System; +use garage_rpc::*; + +use garage_table::replication::{TableReplication, TableShardedReplication}; +use garage_table::table::TABLE_RPC_TIMEOUT; +use garage_table::{PartitionKey, Table}; + +use crate::k2v::causality::*; +use crate::k2v::item_table::*; +use crate::k2v::poll::*; + +/// RPC messages for K2V +#[derive(Debug, Serialize, Deserialize)] +enum K2VRpc { + Ok, + InsertItem(InsertedItem), + InsertManyItems(Vec), + PollItem { + key: PollKey, + causal_context: CausalContext, + timeout_msec: u64, + }, + PollItemResponse(Option), +} + +#[derive(Debug, Serialize, Deserialize)] +struct InsertedItem { + partition: K2VItemPartition, + sort_key: String, + causal_context: Option, + value: DvvsValue, +} + +impl Rpc for K2VRpc { + type Response = Result; +} + +/// The block manager, handling block exchange between nodes, and block storage on local node +pub struct K2VRpcHandler { + system: Arc, + item_table: Arc>, + endpoint: Arc>, + subscriptions: Arc, +} + +impl K2VRpcHandler { + pub fn new( + system: Arc, + item_table: Arc>, + subscriptions: Arc, + ) -> Arc { + let endpoint = system.netapp.endpoint("garage_model/k2v/Rpc".to_string()); + + let rpc_handler = Arc::new(Self { + system, + item_table, + endpoint, + subscriptions, + }); + rpc_handler.endpoint.set_handler(rpc_handler.clone()); + + rpc_handler + } + + // ---- public interface ---- + + pub async fn insert( + &self, + bucket_id: Uuid, + partition_key: String, + sort_key: String, + causal_context: Option, + value: DvvsValue, + ) -> Result<(), Error> { + let partition = K2VItemPartition { + bucket_id, + partition_key, + }; + let mut who = self + .item_table + .data + .replication + .write_nodes(&partition.hash()); + who.sort(); + + self.system + .rpc + .try_call_many( + &self.endpoint, + &who[..], + K2VRpc::InsertItem(InsertedItem { + partition, + sort_key, + causal_context, + value, + }), + RequestStrategy::with_priority(PRIO_NORMAL) + .with_quorum(1) + .with_timeout(TABLE_RPC_TIMEOUT) + .interrupt_after_quorum(true), + ) + .await?; + + Ok(()) + } + + pub async fn insert_batch( + &self, + bucket_id: Uuid, + items: Vec<(String, String, Option, DvvsValue)>, + ) -> Result<(), Error> { + let n_items = items.len(); + + let mut call_list: HashMap<_, Vec<_>> = HashMap::new(); + + for (partition_key, sort_key, causal_context, value) in items { + let partition = K2VItemPartition { + bucket_id, + partition_key, + }; + let mut who = self + .item_table + .data + .replication + .write_nodes(&partition.hash()); + who.sort(); + + call_list.entry(who).or_default().push(InsertedItem { + partition, + sort_key, + causal_context, + value, + }); + } + + debug!( + "K2V insert_batch: {} requests to insert {} items", + call_list.len(), + n_items + ); + let call_futures = call_list.into_iter().map(|(nodes, items)| async move { + let resp = self + .system + .rpc + .try_call_many( + &self.endpoint, + &nodes[..], + K2VRpc::InsertManyItems(items), + RequestStrategy::with_priority(PRIO_NORMAL) + .with_quorum(1) + .with_timeout(TABLE_RPC_TIMEOUT) + .interrupt_after_quorum(true), + ) + .await?; + Ok::<_, Error>((nodes, resp)) + }); + + let mut resps = call_futures.collect::>(); + while let Some(resp) = resps.next().await { + resp?; + } + + Ok(()) + } + + pub async fn poll( + &self, + bucket_id: Uuid, + partition_key: String, + sort_key: String, + causal_context: CausalContext, + timeout_msec: u64, + ) -> Result, Error> { + let poll_key = PollKey { + partition: K2VItemPartition { + bucket_id, + partition_key, + }, + sort_key, + }; + let nodes = self + .item_table + .data + .replication + .write_nodes(&poll_key.partition.hash()); + + let resps = self + .system + .rpc + .try_call_many( + &self.endpoint, + &nodes[..], + K2VRpc::PollItem { + key: poll_key, + causal_context, + timeout_msec, + }, + RequestStrategy::with_priority(PRIO_NORMAL) + .with_quorum(self.item_table.data.replication.read_quorum()) + .with_timeout(Duration::from_millis(timeout_msec) + TABLE_RPC_TIMEOUT), + ) + .await?; + + let mut resp: Option = None; + for v in resps { + match v { + K2VRpc::PollItemResponse(Some(x)) => { + if let Some(y) = &mut resp { + y.merge(&x); + } else { + resp = Some(x); + } + } + K2VRpc::PollItemResponse(None) => { + return Ok(None); + } + v => return Err(Error::unexpected_rpc_message(v)), + } + } + + Ok(resp) + } + + // ---- internal handlers ---- + + async fn handle_insert(&self, item: &InsertedItem) -> Result { + let new = self.local_insert(item)?; + + // Propagate to rest of network + if let Some(updated) = new { + self.item_table.insert(&updated).await?; + } + + Ok(K2VRpc::Ok) + } + + async fn handle_insert_many(&self, items: &[InsertedItem]) -> Result { + let mut updated_vec = vec![]; + + for item in items { + let new = self.local_insert(item)?; + + if let Some(updated) = new { + updated_vec.push(updated); + } + } + + // Propagate to rest of network + if !updated_vec.is_empty() { + self.item_table.insert_many(&updated_vec).await?; + } + + Ok(K2VRpc::Ok) + } + + fn local_insert(&self, item: &InsertedItem) -> Result, Error> { + let tree_key = self + .item_table + .data + .tree_key(&item.partition, &item.sort_key); + + self.item_table + .data + .update_entry_with(&tree_key[..], |ent| { + let mut ent = ent.unwrap_or_else(|| { + K2VItem::new( + item.partition.bucket_id, + item.partition.partition_key.clone(), + item.sort_key.clone(), + ) + }); + ent.update(self.system.id, &item.causal_context, item.value.clone()); + ent + }) + } + + async fn handle_poll(&self, key: &PollKey, ct: &CausalContext) -> Result { + let mut chan = self.subscriptions.subscribe(key); + + let mut value = self + .item_table + .data + .read_entry(&key.partition, &key.sort_key)? + .map(|bytes| self.item_table.data.decode_entry(&bytes[..])) + .transpose()? + .unwrap_or_else(|| { + K2VItem::new( + key.partition.bucket_id, + key.partition.partition_key.clone(), + key.sort_key.clone(), + ) + }); + + while !value.causal_context().is_newer_than(ct) { + value = chan.recv().await?; + } + + Ok(value) + } +} + +#[async_trait] +impl EndpointHandler for K2VRpcHandler { + async fn handle(self: &Arc, message: &K2VRpc, _from: NodeID) -> Result { + match message { + K2VRpc::InsertItem(item) => self.handle_insert(item).await, + K2VRpc::InsertManyItems(items) => self.handle_insert_many(&items[..]).await, + K2VRpc::PollItem { + key, + causal_context, + timeout_msec, + } => { + let delay = tokio::time::sleep(Duration::from_millis(*timeout_msec)); + select! { + ret = self.handle_poll(key, causal_context) => ret.map(Some).map(K2VRpc::PollItemResponse), + _ = delay => Ok(K2VRpc::PollItemResponse(None)), + } + } + m => Err(Error::unexpected_rpc_message(m)), + } + } +} diff --git a/src/model/lib.rs b/src/model/lib.rs index 05a4cdc7..7c9d9270 100644 --- a/src/model/lib.rs +++ b/src/model/lib.rs @@ -3,12 +3,15 @@ extern crate tracing; pub mod permission; -pub mod block_ref_table; +pub mod index_counter; + pub mod bucket_alias_table; pub mod bucket_table; pub mod key_table; -pub mod object_table; -pub mod version_table; + +#[cfg(feature = "k2v")] +pub mod k2v; +pub mod s3; pub mod garage; pub mod helper; diff --git a/src/model/block_ref_table.rs b/src/model/s3/block_ref_table.rs similarity index 85% rename from src/model/block_ref_table.rs rename to src/model/s3/block_ref_table.rs index b6945403..9b3991bf 100644 --- a/src/model/block_ref_table.rs +++ b/src/model/s3/block_ref_table.rs @@ -51,11 +51,11 @@ impl TableSchema for BlockRefTable { type E = BlockRef; type Filter = DeletedFilter; - fn updated(&self, old: Option, new: Option) { + fn updated(&self, old: Option<&Self::E>, new: Option<&Self::E>) { #[allow(clippy::or_fun_call)] - let block = &old.as_ref().or(new.as_ref()).unwrap().block; - let was_before = old.as_ref().map(|x| !x.deleted.get()).unwrap_or(false); - let is_after = new.as_ref().map(|x| !x.deleted.get()).unwrap_or(false); + let block = &old.or(new).unwrap().block; + let was_before = old.map(|x| !x.deleted.get()).unwrap_or(false); + let is_after = new.map(|x| !x.deleted.get()).unwrap_or(false); if is_after && !was_before { if let Err(e) = self.block_manager.block_incref(block) { warn!("block_incref failed for block {:?}: {}", block, e); diff --git a/src/model/s3/mod.rs b/src/model/s3/mod.rs new file mode 100644 index 00000000..4e94337d --- /dev/null +++ b/src/model/s3/mod.rs @@ -0,0 +1,3 @@ +pub mod block_ref_table; +pub mod object_table; +pub mod version_table; diff --git a/src/model/object_table.rs b/src/model/s3/object_table.rs similarity index 98% rename from src/model/object_table.rs rename to src/model/s3/object_table.rs index da53878e..3d9a89f7 100644 --- a/src/model/object_table.rs +++ b/src/model/s3/object_table.rs @@ -9,7 +9,7 @@ use garage_table::crdt::*; use garage_table::replication::TableShardedReplication; use garage_table::*; -use crate::version_table::*; +use crate::s3::version_table::*; use garage_model_050::object_table as old; @@ -232,8 +232,11 @@ impl TableSchema for ObjectTable { type E = Object; type Filter = ObjectFilter; - fn updated(&self, old: Option, new: Option) { + fn updated(&self, old: Option<&Self::E>, new: Option<&Self::E>) { let version_table = self.version_table.clone(); + let old = old.cloned(); + let new = new.cloned(); + self.background.spawn(async move { if let (Some(old_v), Some(new_v)) = (old, new) { // Propagate deletion of old versions diff --git a/src/model/version_table.rs b/src/model/s3/version_table.rs similarity index 96% rename from src/model/version_table.rs rename to src/model/s3/version_table.rs index 839b1f4f..ad096772 100644 --- a/src/model/version_table.rs +++ b/src/model/s3/version_table.rs @@ -8,7 +8,7 @@ use garage_table::crdt::*; use garage_table::replication::TableShardedReplication; use garage_table::*; -use crate::block_ref_table::*; +use crate::s3::block_ref_table::*; use garage_model_050::version_table as old; @@ -137,8 +137,11 @@ impl TableSchema for VersionTable { type E = Version; type Filter = DeletedFilter; - fn updated(&self, old: Option, new: Option) { + fn updated(&self, old: Option<&Self::E>, new: Option<&Self::E>) { let block_ref_table = self.block_ref_table.clone(); + let old = old.cloned(); + let new = new.cloned(); + self.background.spawn(async move { if let (Some(old_v), Some(new_v)) = (old, new) { // Propagate deletion of version blocks diff --git a/src/rpc/Cargo.toml b/src/rpc/Cargo.toml index 46d0dc1e..bed7f44a 100644 --- a/src/rpc/Cargo.toml +++ b/src/rpc/Cargo.toml @@ -52,5 +52,6 @@ netapp = { version = "0.4.4", features = ["telemetry"] } hyper = { version = "0.14", features = ["client", "http1", "runtime", "tcp"] } + [features] kubernetes-discovery = [ "kube", "k8s-openapi", "openssl", "schemars" ] diff --git a/src/table/data.rs b/src/table/data.rs index ff7965f5..5cb10066 100644 --- a/src/table/data.rs +++ b/src/table/data.rs @@ -1,8 +1,9 @@ use core::borrow::Borrow; +use std::convert::TryInto; use std::sync::Arc; use serde_bytes::ByteBuf; -use sled::Transactional; +use sled::{IVec, Transactional}; use tokio::sync::Notify; use garage_util::data::*; @@ -16,12 +17,13 @@ use crate::gc::GcTodoEntry; use crate::metrics::*; use crate::replication::*; use crate::schema::*; +use crate::util::*; pub struct TableData { system: Arc, - pub(crate) instance: F, - pub(crate) replication: R, + pub instance: F, + pub replication: R, pub store: sled::Tree, @@ -83,18 +85,48 @@ where pub fn read_range( &self, - p: &F::P, - s: &Option, + partition_key: &F::P, + start: &Option, + filter: &Option, + limit: usize, + enumeration_order: EnumerationOrder, + ) -> Result>, Error> { + let partition_hash = partition_key.hash(); + match enumeration_order { + EnumerationOrder::Forward => { + let first_key = match start { + None => partition_hash.to_vec(), + Some(sk) => self.tree_key(partition_key, sk), + }; + let range = self.store.range(first_key..); + self.read_range_aux(partition_hash, range, filter, limit) + } + EnumerationOrder::Reverse => match start { + Some(sk) => { + let last_key = self.tree_key(partition_key, sk); + let range = self.store.range(..=last_key).rev(); + self.read_range_aux(partition_hash, range, filter, limit) + } + None => { + let mut last_key = partition_hash.to_vec(); + let lower = u128::from_be_bytes(last_key[16..32].try_into().unwrap()); + last_key[16..32].copy_from_slice(&u128::to_be_bytes(lower + 1)); + let range = self.store.range(..last_key).rev(); + self.read_range_aux(partition_hash, range, filter, limit) + } + }, + } + } + + fn read_range_aux( + &self, + partition_hash: Hash, + range: impl Iterator>, filter: &Option, limit: usize, ) -> Result>, Error> { - let partition_hash = p.hash(); - let first_key = match s { - None => partition_hash.to_vec(), - Some(sk) => self.tree_key(p, sk), - }; let mut ret = vec![]; - for item in self.store.range(first_key..) { + for item in range { let (key, value) = item?; if &key[..32] != partition_hash.as_slice() { break; @@ -136,17 +168,31 @@ where let update = self.decode_entry(update_bytes)?; let tree_key = self.tree_key(update.partition_key(), update.sort_key()); + self.update_entry_with(&tree_key[..], |ent| match ent { + Some(mut ent) => { + ent.merge(&update); + ent + } + None => update.clone(), + })?; + Ok(()) + } + + pub fn update_entry_with( + &self, + tree_key: &[u8], + f: impl Fn(Option) -> F::E, + ) -> Result, Error> { let changed = (&self.store, &self.merkle_todo).transaction(|(store, mkl_todo)| { - let (old_entry, old_bytes, new_entry) = match store.get(&tree_key)? { + let (old_entry, old_bytes, new_entry) = match store.get(tree_key)? { Some(old_bytes) => { let old_entry = self .decode_entry(&old_bytes) .map_err(sled::transaction::ConflictableTransactionError::Abort)?; - let mut new_entry = old_entry.clone(); - new_entry.merge(&update); + let new_entry = f(Some(old_entry.clone())); (Some(old_entry), Some(old_bytes), new_entry) } - None => (None, None, update.clone()), + None => (None, None, f(None)), }; // Scenario 1: the value changed, so of course there is a change @@ -163,8 +209,8 @@ where if value_changed || encoding_changed { let new_bytes_hash = blake2sum(&new_bytes[..]); - mkl_todo.insert(tree_key.clone(), new_bytes_hash.as_slice())?; - store.insert(tree_key.clone(), new_bytes)?; + mkl_todo.insert(tree_key.to_vec(), new_bytes_hash.as_slice())?; + store.insert(tree_key.to_vec(), new_bytes)?; Ok(Some((old_entry, new_entry, new_bytes_hash))) } else { Ok(None) @@ -175,7 +221,7 @@ where self.metrics.internal_update_counter.add(1); let is_tombstone = new_entry.is_tombstone(); - self.instance.updated(old_entry, Some(new_entry)); + self.instance.updated(old_entry.as_ref(), Some(&new_entry)); self.merkle_todo_notify.notify_one(); if is_tombstone { // We are only responsible for GC'ing this item if we are the @@ -187,12 +233,14 @@ where let pk_hash = Hash::try_from(&tree_key[..32]).unwrap(); let nodes = self.replication.write_nodes(&pk_hash); if nodes.first() == Some(&self.system.id) { - GcTodoEntry::new(tree_key, new_bytes_hash).save(&self.gc_todo)?; + GcTodoEntry::new(tree_key.to_vec(), new_bytes_hash).save(&self.gc_todo)?; } } - } - Ok(()) + Ok(Some(new_entry)) + } else { + Ok(None) + } } pub(crate) fn delete_if_equal(self: &Arc, k: &[u8], v: &[u8]) -> Result { @@ -211,7 +259,7 @@ where self.metrics.internal_delete_counter.add(1); let old_entry = self.decode_entry(v)?; - self.instance.updated(Some(old_entry), None); + self.instance.updated(Some(&old_entry), None); self.merkle_todo_notify.notify_one(); } Ok(removed) @@ -235,7 +283,7 @@ where if let Some(old_v) = removed { let old_entry = self.decode_entry(&old_v[..])?; - self.instance.updated(Some(old_entry), None); + self.instance.updated(Some(&old_entry), None); self.merkle_todo_notify.notify_one(); Ok(true) } else { @@ -245,13 +293,13 @@ where // ---- Utility functions ---- - pub(crate) fn tree_key(&self, p: &F::P, s: &F::S) -> Vec { + pub fn tree_key(&self, p: &F::P, s: &F::S) -> Vec { let mut ret = p.hash().to_vec(); ret.extend(s.sort_key()); ret } - pub(crate) fn decode_entry(&self, bytes: &[u8]) -> Result { + pub fn decode_entry(&self, bytes: &[u8]) -> Result { match rmp_serde::decode::from_read_ref::<_, F::E>(bytes) { Ok(x) => Ok(x), Err(e) => match F::try_migrate(bytes) { diff --git a/src/table/schema.rs b/src/table/schema.rs index eba918a2..37327037 100644 --- a/src/table/schema.rs +++ b/src/table/schema.rs @@ -86,7 +86,7 @@ pub trait TableSchema: Send + Sync { // as the update itself is an unchangeable fact that will never go back // due to CRDT logic. Typically errors in propagation of info should be logged // to stderr. - fn updated(&self, _old: Option, _new: Option) {} + fn updated(&self, _old: Option<&Self::E>, _new: Option<&Self::E>) {} fn matches_filter(entry: &Self::E, filter: &Self::Filter) -> bool; } diff --git a/src/table/table.rs b/src/table/table.rs index 7f87a449..2a167604 100644 --- a/src/table/table.rs +++ b/src/table/table.rs @@ -1,4 +1,5 @@ -use std::collections::{BTreeMap, HashMap}; +use std::borrow::Borrow; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::sync::Arc; use std::time::Duration; @@ -26,8 +27,9 @@ use crate::merkle::*; use crate::replication::*; use crate::schema::*; use crate::sync::*; +use crate::util::*; -const TABLE_RPC_TIMEOUT: Duration = Duration::from_secs(10); +pub const TABLE_RPC_TIMEOUT: Duration = Duration::from_secs(10); pub struct Table { pub system: Arc, @@ -45,7 +47,13 @@ pub(crate) enum TableRpc { ReadEntryResponse(Option), // Read range: read all keys in partition P, possibly starting at a certain sort key offset - ReadRange(F::P, Option, Option, usize), + ReadRange { + partition: F::P, + begin_sort_key: Option, + filter: Option, + limit: usize, + enumeration_order: EnumerationOrder, + }, Update(Vec>), } @@ -123,9 +131,13 @@ where Ok(()) } - pub async fn insert_many(&self, entries: &[F::E]) -> Result<(), Error> { + pub async fn insert_many(&self, entries: I) -> Result<(), Error> + where + I: IntoIterator + Send + Sync, + IE: Borrow + Send + Sync, + { let tracer = opentelemetry::global::tracer("garage_table"); - let span = tracer.start(format!("{} insert_many {}", F::TABLE_NAME, entries.len())); + let span = tracer.start(format!("{} insert_many", F::TABLE_NAME)); self.insert_many_internal(entries) .bound_record_duration(&self.data.metrics.put_request_duration) @@ -137,10 +149,15 @@ where Ok(()) } - async fn insert_many_internal(&self, entries: &[F::E]) -> Result<(), Error> { + async fn insert_many_internal(&self, entries: I) -> Result<(), Error> + where + I: IntoIterator + Send + Sync, + IE: Borrow + Send + Sync, + { let mut call_list: HashMap<_, Vec<_>> = HashMap::new(); - for entry in entries.iter() { + for entry in entries.into_iter() { + let entry = entry.borrow(); let hash = entry.partition_key().hash(); let who = self.data.replication.write_nodes(&hash); let e_enc = Arc::new(ByteBuf::from(rmp_to_vec_all_named(entry)?)); @@ -261,12 +278,19 @@ where begin_sort_key: Option, filter: Option, limit: usize, + enumeration_order: EnumerationOrder, ) -> Result, Error> { let tracer = opentelemetry::global::tracer("garage_table"); let span = tracer.start(format!("{} get_range", F::TABLE_NAME)); let res = self - .get_range_internal(partition_key, begin_sort_key, filter, limit) + .get_range_internal( + partition_key, + begin_sort_key, + filter, + limit, + enumeration_order, + ) .bound_record_duration(&self.data.metrics.get_request_duration) .with_context(Context::current_with_span(span)) .await?; @@ -282,11 +306,18 @@ where begin_sort_key: Option, filter: Option, limit: usize, + enumeration_order: EnumerationOrder, ) -> Result, Error> { let hash = partition_key.hash(); let who = self.data.replication.read_nodes(&hash); - let rpc = TableRpc::::ReadRange(partition_key.clone(), begin_sort_key, filter, limit); + let rpc = TableRpc::::ReadRange { + partition: partition_key.clone(), + begin_sort_key, + filter, + limit, + enumeration_order, + }; let resps = self .system @@ -302,44 +333,65 @@ where ) .await?; - let mut ret = BTreeMap::new(); - let mut to_repair = BTreeMap::new(); + let mut ret: BTreeMap, F::E> = BTreeMap::new(); + let mut to_repair = BTreeSet::new(); for resp in resps { if let TableRpc::Update(entries) = resp { for entry_bytes in entries.iter() { let entry = self.data.decode_entry(entry_bytes.as_slice())?; let entry_key = self.data.tree_key(entry.partition_key(), entry.sort_key()); - match ret.remove(&entry_key) { - None => { - ret.insert(entry_key, Some(entry)); - } - Some(Some(mut prev)) => { - let must_repair = prev != entry; - prev.merge(&entry); - if must_repair { - to_repair.insert(entry_key.clone(), Some(prev.clone())); + match ret.get_mut(&entry_key) { + Some(e) => { + if *e != entry { + e.merge(&entry); + to_repair.insert(entry_key.clone()); } - ret.insert(entry_key, Some(prev)); } - Some(None) => unreachable!(), + None => { + ret.insert(entry_key, entry); + } } } + } else { + return Err(Error::unexpected_rpc_message(resp)); } } + if !to_repair.is_empty() { let self2 = self.clone(); + let to_repair = to_repair + .into_iter() + .map(|k| ret.get(&k).unwrap().clone()) + .collect::>(); self.system.background.spawn_cancellable(async move { - for (_, v) in to_repair.iter_mut() { - self2.repair_on_read(&who[..], v.take().unwrap()).await?; + for v in to_repair { + self2.repair_on_read(&who[..], v).await?; } Ok(()) }); } - let ret_vec = ret - .iter_mut() - .take(limit) - .map(|(_k, v)| v.take().unwrap()) - .collect::>(); + + // At this point, the `ret` btreemap might contain more than `limit` + // items, because nodes might have returned us each `limit` items + // but for different keys. We have to take only the first `limit` items + // in this map, in the specified enumeration order, for two reasons: + // 1. To return to the user no more than the number of items that they requested + // 2. To return only items for which we have a read quorum: we do not know + // that we have a read quorum for the items after the first `limit` + // of them + let ret_vec = match enumeration_order { + EnumerationOrder::Forward => ret + .into_iter() + .take(limit) + .map(|(_k, v)| v) + .collect::>(), + EnumerationOrder::Reverse => ret + .into_iter() + .rev() + .take(limit) + .map(|(_k, v)| v) + .collect::>(), + }; Ok(ret_vec) } @@ -378,8 +430,20 @@ where let value = self.data.read_entry(key, sort_key)?; Ok(TableRpc::ReadEntryResponse(value)) } - TableRpc::ReadRange(key, begin_sort_key, filter, limit) => { - let values = self.data.read_range(key, begin_sort_key, filter, *limit)?; + TableRpc::ReadRange { + partition, + begin_sort_key, + filter, + limit, + enumeration_order, + } => { + let values = self.data.read_range( + partition, + begin_sort_key, + filter, + *limit, + *enumeration_order, + )?; Ok(TableRpc::Update(values)) } TableRpc::Update(pairs) => { diff --git a/src/table/util.rs b/src/table/util.rs index 2a5c3afe..20595a94 100644 --- a/src/table/util.rs +++ b/src/table/util.rs @@ -17,7 +17,7 @@ impl PartitionKey for EmptyKey { } } -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum DeletedFilter { Any, Deleted, @@ -33,3 +33,19 @@ impl DeletedFilter { } } } + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum EnumerationOrder { + Forward, + Reverse, +} + +impl EnumerationOrder { + pub fn from_reverse(reverse: bool) -> Self { + if reverse { + Self::Reverse + } else { + Self::Forward + } + } +} diff --git a/src/util/Cargo.toml b/src/util/Cargo.toml index f13c1589..95cde531 100644 --- a/src/util/Cargo.toml +++ b/src/util/Cargo.toml @@ -41,3 +41,6 @@ http = "0.2" hyper = "0.14" opentelemetry = { version = "0.17", features = [ "rt-tokio", "metrics", "trace" ] } + +[features] +k2v = [] diff --git a/src/util/config.rs b/src/util/config.rs index e4d96476..4d66bfe4 100644 --- a/src/util/config.rs +++ b/src/util/config.rs @@ -73,7 +73,11 @@ pub struct Config { pub sled_flush_every_ms: u64, /// Configuration for S3 api - pub s3_api: ApiConfig, + pub s3_api: S3ApiConfig, + + /// Configuration for K2V api + #[cfg(feature = "k2v")] + pub k2v_api: Option, /// Configuration for serving files as normal web server pub s3_web: WebConfig, @@ -85,7 +89,7 @@ pub struct Config { /// Configuration for S3 api #[derive(Deserialize, Debug, Clone)] -pub struct ApiConfig { +pub struct S3ApiConfig { /// Address and port to bind for api serving pub api_bind_addr: SocketAddr, /// S3 region to use @@ -95,6 +99,14 @@ pub struct ApiConfig { pub root_domain: Option, } +/// Configuration for K2V api +#[cfg(feature = "k2v")] +#[derive(Deserialize, Debug, Clone)] +pub struct K2VApiConfig { + /// Address and port to bind for api serving + pub api_bind_addr: SocketAddr, +} + /// Configuration for serving files as normal web server #[derive(Deserialize, Debug, Clone)] pub struct WebConfig { diff --git a/src/util/error.rs b/src/util/error.rs index bdb3a69b..8734a0c8 100644 --- a/src/util/error.rs +++ b/src/util/error.rs @@ -44,6 +44,9 @@ pub enum Error { #[error(display = "Tokio semaphore acquire error: {}", _0)] TokioSemAcquire(#[error(source)] tokio::sync::AcquireError), + #[error(display = "Tokio broadcast receive error: {}", _0)] + TokioBcastRecv(#[error(source)] tokio::sync::broadcast::error::RecvError), + #[error(display = "Remote error: {}", _0)] RemoteError(String), diff --git a/src/web/web_server.rs b/src/web/web_server.rs index c3d691d0..867adc51 100644 --- a/src/web/web_server.rs +++ b/src/web/web_server.rs @@ -20,8 +20,8 @@ use crate::error::*; use garage_api::error::{Error as ApiError, OkOrBadRequest, OkOrInternalError}; use garage_api::helpers::{authority_to_host, host_to_bucket}; -use garage_api::s3_cors::{add_cors_headers, find_matching_cors_rule, handle_options_for_bucket}; -use garage_api::s3_get::{handle_get, handle_head}; +use garage_api::s3::cors::{add_cors_headers, find_matching_cors_rule, handle_options_for_bucket}; +use garage_api::s3::get::{handle_get, handle_head}; use garage_model::garage::Garage; From 176715c5b27ea62e3b1bf77356360b5086d671e2 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 16 May 2022 11:54:37 +0200 Subject: [PATCH 004/149] Fix ReadIndex spec and add JSON5 remark to doc --- doc/drafts/k2v-spec.md | 51 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/doc/drafts/k2v-spec.md b/doc/drafts/k2v-spec.md index 08809069..175bb02e 100644 --- a/doc/drafts/k2v-spec.md +++ b/doc/drafts/k2v-spec.md @@ -195,6 +195,10 @@ TO UNDERSTAND IN ORDER TO USE IT CORRECTLY.** ## API Endpoints +**Remark.** Example queries and responses here are given in JSON5 format +for clarity. However the actual K2V API uses basic JSON so all examples +and responses need to be translated. + ### Operations on single items **ReadItem: `GET //?sort_key=`** @@ -370,8 +374,11 @@ HTTP/1.1 204 NO CONTENT **ReadIndex: `GET /?start=&end=&limit=`** Lists all partition keys in the bucket for which some triplets exist, and gives -for each the number of triplets (or an approximation thereof, this value is - asynchronously updated, and thus eventually consistent). +for each the number of triplets, total number of values (which might be bigger +than the number of triplets in case of conflicts), total number of bytes of +these values, and number of triplets that are in a state of conflict. +The values returned are an approximation of the true counts in the bucket, +as these values are asynchronously updated, and thus eventually consistent. Query parameters: @@ -426,11 +433,41 @@ HTTP/1.1 200 OK limit: null, reverse: false, partitionKeys: [ - { pk: "keys", n: 3043 }, - { pk: "mailbox:INBOX", n: 42 }, - { pk: "mailbox:Junk", n: 2991 }, - { pk: "mailbox:Trash", n: 10 }, - { pk: "mailboxes", n: 3 }, + { + pk: "keys", + entries: 3043, + conflicts: 0, + values: 3043, + bytes: 121720, + }, + { + pk: "mailbox:INBOX", + entries: 42, + conflicts: 1, + values: 43, + bytes: 142029, + }, + { + pk: "mailbox:Junk", + entries: 2991 + conflicts: 0, + values: 2991, + bytes: 12019322, + }, + { + pk: "mailbox:Trash", + entries: 10, + conflicts: 0, + values: 10, + bytes: 32401, + }, + { + pk: "mailboxes", + entries: 3, + conflicts: 0, + values: 3, + bytes: 3019, + }, ], more: false, nextStart: null, From 7b474855e3a8491fcdde69d12d3fbae27f520383 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Thu, 5 May 2022 10:56:44 +0200 Subject: [PATCH 005/149] Make background runner terminate correctly --- src/util/background.rs | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/util/background.rs b/src/util/background.rs index bfdaaf1e..d35425f5 100644 --- a/src/util/background.rs +++ b/src/util/background.rs @@ -6,7 +6,9 @@ use std::time::Duration; use futures::future::*; use futures::select; -use tokio::sync::{mpsc, watch, Mutex}; +use futures::stream::FuturesUnordered; +use futures::StreamExt; +use tokio::sync::{mpsc, mpsc::error::TryRecvError, watch, Mutex}; use crate::error::Error; @@ -30,26 +32,31 @@ impl BackgroundRunner { let stop_signal_2 = stop_signal.clone(); let await_all_done = tokio::spawn(async move { + let mut workers = FuturesUnordered::new(); + let mut shutdown_timer = 0; loop { - let wkr = { - select! { - item = worker_out.recv().fuse() => { - match item { - Some(x) => x, - None => break, - } + let closed = match worker_out.try_recv() { + Ok(wkr) => { + workers.push(wkr); + false + } + Err(TryRecvError::Empty) => false, + Err(TryRecvError::Disconnected) => true, + }; + select! { + res = workers.next() => { + if let Some(Err(e)) = res { + error!("Worker exited with error: {}", e); } - _ = tokio::time::sleep(Duration::from_secs(5)).fuse() => { - if *stop_signal_2.borrow() { + } + _ = tokio::time::sleep(Duration::from_secs(1)).fuse() => { + if closed || *stop_signal_2.borrow() { + shutdown_timer += 1; + if shutdown_timer >= 10 { break; - } else { - continue; } } } - }; - if let Err(e) = wkr.await { - error!("Error while awaiting for worker: {}", e); } } }); From c692f55d5ce2c3ed08db7fbc4844debcc0aeb134 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 17 May 2022 11:50:23 +0200 Subject: [PATCH 006/149] K2V: Fix `end` parameter and add tests (fix #305) --- src/api/k2v/range.rs | 6 ++- src/garage/tests/k2v/batch.rs | 89 ++++++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/src/api/k2v/range.rs b/src/api/k2v/range.rs index cd019723..295c34aa 100644 --- a/src/api/k2v/range.rs +++ b/src/api/k2v/range.rs @@ -74,7 +74,11 @@ where } } if let Some(e) = end { - if entry.sort_key() == e { + let is_finished = match enumeration_order { + EnumerationOrder::Forward => entry.sort_key() >= e, + EnumerationOrder::Reverse => entry.sort_key() <= e, + }; + if is_finished { return Ok((entries, false, None)); } } diff --git a/src/garage/tests/k2v/batch.rs b/src/garage/tests/k2v/batch.rs index 1182a298..acae1910 100644 --- a/src/garage/tests/k2v/batch.rs +++ b/src/garage/tests/k2v/batch.rs @@ -92,7 +92,9 @@ async fn test_batch() { br#"[ {"partitionKey": "root"}, {"partitionKey": "root", "start": "c"}, + {"partitionKey": "root", "start": "c", "end": "dynamite"}, {"partitionKey": "root", "start": "c", "reverse": true, "end": "a"}, + {"partitionKey": "root", "start": "c", "reverse": true, "end": "azerty"}, {"partitionKey": "root", "limit": 1}, {"partitionKey": "root", "prefix": "d"} ]"# @@ -147,6 +149,24 @@ async fn test_batch() { "more": false, "nextStart": null, }, + { + "partitionKey": "root", + "prefix": null, + "start": "c", + "end": "dynamite", + "limit": null, + "reverse": false, + "conflictsOnly": false, + "tombstones": false, + "singleItem": false, + "items": [ + {"sk": "c", "ct": ct.get("c").unwrap(), "v": [base64::encode(values.get("c").unwrap())]}, + {"sk": "d.1", "ct": ct.get("d.1").unwrap(), "v": [base64::encode(values.get("d.1").unwrap())]}, + {"sk": "d.2", "ct": ct.get("d.2").unwrap(), "v": [base64::encode(values.get("d.2").unwrap())]}, + ], + "more": false, + "nextStart": null, + }, { "partitionKey": "root", "prefix": null, @@ -164,6 +184,23 @@ async fn test_batch() { "more": false, "nextStart": null, }, + { + "partitionKey": "root", + "prefix": null, + "start": "c", + "end": "azerty", + "limit": null, + "reverse": true, + "conflictsOnly": false, + "tombstones": false, + "singleItem": false, + "items": [ + {"sk": "c", "ct": ct.get("c").unwrap(), "v": [base64::encode(values.get("c").unwrap())]}, + {"sk": "b", "ct": ct.get("b").unwrap(), "v": [base64::encode(values.get("b").unwrap())]}, + ], + "more": false, + "nextStart": null, + }, { "partitionKey": "root", "prefix": null, @@ -465,6 +502,34 @@ async fn test_batch() { ]) ); + // update our known tombstones + for sk in ["a", "b", "d.1", "d.2"] { + let res = ctx + .k2v + .request + .builder(bucket.clone()) + .path("root") + .query_param("sort_key", Some(sk)) + .signed_header("accept", "application/octet-stream") + .send() + .await + .unwrap(); + assert_eq!(res.status(), 204); + assert_eq!( + res.headers().get("content-type").unwrap().to_str().unwrap(), + "application/octet-stream" + ); + ct.insert( + sk, + res.headers() + .get("x-garage-causality-token") + .unwrap() + .to_str() + .unwrap() + .to_string(), + ); + } + let res = ctx .k2v .request @@ -473,7 +538,8 @@ async fn test_batch() { .body( br#"[ {"partitionKey": "root"}, - {"partitionKey": "root", "reverse": true} + {"partitionKey": "root", "reverse": true}, + {"partitionKey": "root", "tombstones": true} ]"# .to_vec(), ) @@ -520,6 +586,27 @@ async fn test_batch() { "more": false, "nextStart": null, }, + { + "partitionKey": "root", + "prefix": null, + "start": null, + "end": null, + "limit": null, + "reverse": false, + "conflictsOnly": false, + "tombstones": true, + "singleItem": false, + "items": [ + {"sk": "a", "ct": ct.get("a").unwrap(), "v": [null]}, + {"sk": "b", "ct": ct.get("b").unwrap(), "v": [null]}, + {"sk": "c", "ct": ct.get("c").unwrap(), "v": [base64::encode(values.get("c").unwrap()), base64::encode(values.get("c'").unwrap())]}, + {"sk": "d.1", "ct": ct.get("d.1").unwrap(), "v": [null]}, + {"sk": "d.2", "ct": ct.get("d.2").unwrap(), "v": [null]}, + {"sk": "e", "ct": ct.get("e").unwrap(), "v": [base64::encode(values.get("e").unwrap())]}, + ], + "more": false, + "nextStart": null, + }, ]) ); } From 64c193e3dbb536d5d3c2881bc9aebbb3e4e6272e Mon Sep 17 00:00:00 2001 From: trinity-1686a Date: Wed, 18 May 2022 22:24:09 +0200 Subject: [PATCH 007/149] Add a K2V client library and CLI (#303) lib.rs could use getting split in modules, but I'm not sure how exactly Co-authored-by: trinity-1686a Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/303 Co-authored-by: trinity-1686a Co-committed-by: trinity-1686a --- Cargo.lock | 213 ++++++++++- Cargo.nix | 537 ++++++++++++++++++++-------- Cargo.toml | 5 +- shell.nix | 2 + src/garage/cli/cmd.rs | 1 + src/garage/cli/layout.rs | 1 + src/garage/cli/util.rs | 30 +- src/k2v-client/Cargo.toml | 27 ++ src/k2v-client/README.md | 25 ++ src/k2v-client/src/bin/k2v-cli.rs | 466 ++++++++++++++++++++++++ src/k2v-client/src/error.rs | 22 ++ src/k2v-client/src/lib.rs | 566 ++++++++++++++++++++++++++++++ src/util/formater.rs | 28 ++ src/util/lib.rs | 1 + 14 files changed, 1723 insertions(+), 201 deletions(-) create mode 100644 src/k2v-client/Cargo.toml create mode 100644 src/k2v-client/README.md create mode 100644 src/k2v-client/src/bin/k2v-cli.rs create mode 100644 src/k2v-client/src/error.rs create mode 100644 src/k2v-client/src/lib.rs create mode 100644 src/util/formater.rs diff --git a/Cargo.lock b/Cargo.lock index de1ae5cd..a4476273 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -403,10 +403,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "bitflags", - "textwrap", + "textwrap 0.11.0", "unicode-width", ] +[[package]] +name = "clap" +version = "3.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "lazy_static", + "strsim", + "termcolor", + "textwrap 0.15.0", +] + +[[package]] +name = "clap_derive" +version = "3.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +dependencies = [ + "heck 0.4.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -504,6 +543,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "ct-logs" version = "0.8.0" @@ -848,7 +897,7 @@ dependencies = [ "garage_web", "git-version", "hex", - "hmac", + "hmac 0.10.1", "http", "hyper", "kuska-sodiumoxide", @@ -904,7 +953,7 @@ dependencies = [ "garage_table 0.7.0", "garage_util 0.7.0", "hex", - "hmac", + "hmac 0.10.1", "http", "http-range", "httpdate 0.3.2", @@ -1261,6 +1310,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1296,6 +1351,16 @@ dependencies = [ "digest", ] +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac 0.11.1", + "digest", +] + [[package]] name = "http" version = "0.2.6" @@ -1523,6 +1588,23 @@ dependencies = [ "serde_json", ] +[[package]] +name = "k2v-client" +version = "0.1.0" +dependencies = [ + "base64", + "clap 3.1.18", + "garage_util 0.7.0", + "http", + "rusoto_core", + "rusoto_credential", + "rusoto_signature", + "serde", + "serde_json", + "thiserror", + "tokio", +] + [[package]] name = "k8s-openapi" version = "0.13.1" @@ -2057,6 +2139,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "os_str_bytes" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d8d0b2f198229de29dca79676f2738ff952edf3fde542eb8bf94d8c21b435" + [[package]] name = "parking_lot" version = "0.11.2" @@ -2306,7 +2394,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" dependencies = [ "bytes 1.1.0", - "heck", + "heck 0.3.3", "itertools 0.10.3", "lazy_static", "log", @@ -2533,6 +2621,75 @@ dependencies = [ "xmlparser", ] +[[package]] +name = "rusoto_core" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db30db44ea73551326269adcf7a2169428a054f14faf9e1768f2163494f2fa2" +dependencies = [ + "async-trait", + "base64", + "bytes 1.1.0", + "crc32fast", + "futures", + "http", + "hyper", + "hyper-tls", + "lazy_static", + "log", + "rusoto_credential", + "rusoto_signature", + "rustc_version", + "serde", + "serde_json", + "tokio", + "xml-rs", +] + +[[package]] +name = "rusoto_credential" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee0a6c13db5aad6047b6a44ef023dbbc21a056b6dab5be3b79ce4283d5c02d05" +dependencies = [ + "async-trait", + "chrono", + "dirs-next", + "futures", + "hyper", + "serde", + "serde_json", + "shlex", + "tokio", + "zeroize", +] + +[[package]] +name = "rusoto_signature" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ae95491c8b4847931e291b151127eccd6ff8ca13f33603eb3d0035ecb05272" +dependencies = [ + "base64", + "bytes 1.1.0", + "chrono", + "digest", + "futures", + "hex", + "hmac 0.11.0", + "http", + "hyper", + "log", + "md-5", + "percent-encoding", + "pin-project-lite", + "rusoto_credential", + "rustc_version", + "serde", + "sha2", + "tokio", +] + [[package]] name = "rustc_version" version = "0.4.0" @@ -2669,9 +2826,9 @@ checksum = "a4a3381e03edd24287172047536f20cabde766e2cd3e65e6b00fb3af51c4f38d" [[package]] name = "serde" -version = "1.0.136" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" dependencies = [ "serde_derive", ] @@ -2697,9 +2854,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.136" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" dependencies = [ "proc-macro2", "quote", @@ -2719,9 +2876,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.79" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" dependencies = [ "indexmap", "itoa", @@ -2754,6 +2911,12 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -2876,7 +3039,7 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" dependencies = [ - "clap", + "clap 2.34.0", "lazy_static", "structopt-derive", ] @@ -2887,7 +3050,7 @@ version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" dependencies = [ - "heck", + "heck 0.3.3", "proc-macro-error", "proc-macro2", "quote", @@ -2902,9 +3065,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.89" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea297be220d52398dcc07ce15a209fce436d361735ac1db700cab3b6cdfb9f54" +checksum = "a07e33e919ebcd69113d5be0e4d70c5707004ff45188910106854f38b960df4a" dependencies = [ "proc-macro2", "quote", @@ -2956,19 +3119,25 @@ dependencies = [ ] [[package]] -name = "thiserror" -version = "1.0.30" +name = "textwrap" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.30" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" dependencies = [ "proc-macro2", "quote", @@ -3535,6 +3704,12 @@ version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + [[package]] name = "xmlparser" version = "0.13.3" diff --git a/Cargo.nix b/Cargo.nix index 39f409b6..9c936ed0 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -13,6 +13,7 @@ args@{ "garage_api/default" "garage_web/default" "garage/default" + "k2v-client/default" ], rustPackages, buildRustPackages, @@ -54,6 +55,7 @@ in garage_api = rustPackages.unknown.garage_api."0.7.0"; garage_web = rustPackages.unknown.garage_web."0.7.0"; garage = rustPackages.unknown.garage."0.7.0"; + k2v-client = rustPackages.unknown.k2v-client."0.1.0"; }; "registry+https://github.com/rust-lang/crates.io-index".aho-corasick."0.7.18" = overridableMkRustCrate (profileName: rec { name = "aho-corasick"; @@ -104,8 +106,8 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "50f1c3703dd33532d7f0ca049168930e9099ecac238e23cf932f3a69c42f06da"; }; dependencies = { - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; - serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; }; }); @@ -128,7 +130,7 @@ in dependencies = { proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; }); @@ -140,7 +142,7 @@ in dependencies = { proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; }); @@ -239,7 +241,7 @@ in aws_smithy_http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-http."0.38.0" { inherit profileName; }; aws_types = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-types."0.8.0" { inherit profileName; }; http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; - thiserror = rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.30" { inherit profileName; }; + thiserror = rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.31" { inherit profileName; }; tracing = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; }; }); @@ -400,7 +402,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "7aa6c9de6c3f875faabcaaad1fb1f4ef241683bfc22795f731719e3568c3ca9f"; }; dependencies = { - thiserror = rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.30" { inherit profileName; }; + thiserror = rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.31" { inherit profileName; }; xmlparser = rustPackages."registry+https://github.com/rust-lang/crates.io-index".xmlparser."0.13.3" { inherit profileName; }; }; }); @@ -565,7 +567,7 @@ in [ "default" ] [ "libc" ] [ "oldtime" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "serde") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "serde") [ "std" ] [ "time" ] [ "winapi" ] @@ -574,7 +576,7 @@ in libc = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; num_integer = rustPackages."registry+https://github.com/rust-lang/crates.io-index".num-integer."0.1.44" { inherit profileName; }; num_traits = rustPackages."registry+https://github.com/rust-lang/crates.io-index".num-traits."0.2.14" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; time = rustPackages."registry+https://github.com/rust-lang/crates.io-index".time."0.1.44" { inherit profileName; }; ${ if hostPlatform.isWindows then "winapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".winapi."0.3.9" { inherit profileName; }; }; @@ -592,6 +594,64 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".clap."3.1.18" = overridableMkRustCrate (profileName: rec { + name = "clap"; + version = "3.1.18"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b"; }; + features = builtins.concatLists [ + [ "atty" ] + [ "clap_derive" ] + [ "color" ] + [ "default" ] + [ "derive" ] + [ "env" ] + [ "lazy_static" ] + [ "std" ] + [ "strsim" ] + [ "suggestions" ] + [ "termcolor" ] + ]; + dependencies = { + atty = rustPackages."registry+https://github.com/rust-lang/crates.io-index".atty."0.2.14" { inherit profileName; }; + bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; + clap_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".clap_derive."3.1.18" { profileName = "__noProfile"; }; + clap_lex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".clap_lex."0.2.0" { inherit profileName; }; + indexmap = rustPackages."registry+https://github.com/rust-lang/crates.io-index".indexmap."1.8.0" { inherit profileName; }; + lazy_static = rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { inherit profileName; }; + strsim = rustPackages."registry+https://github.com/rust-lang/crates.io-index".strsim."0.10.0" { inherit profileName; }; + termcolor = rustPackages."registry+https://github.com/rust-lang/crates.io-index".termcolor."1.1.3" { inherit profileName; }; + textwrap = rustPackages."registry+https://github.com/rust-lang/crates.io-index".textwrap."0.15.0" { inherit profileName; }; + }; + }); + + "registry+https://github.com/rust-lang/crates.io-index".clap_derive."3.1.18" = overridableMkRustCrate (profileName: rec { + name = "clap_derive"; + version = "3.1.18"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c"; }; + features = builtins.concatLists [ + [ "default" ] + ]; + dependencies = { + heck = rustPackages."registry+https://github.com/rust-lang/crates.io-index".heck."0.4.0" { inherit profileName; }; + proc_macro_error = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro-error."1.0.4" { inherit profileName; }; + proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; + quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; + }; + }); + + "registry+https://github.com/rust-lang/crates.io-index".clap_lex."0.2.0" = overridableMkRustCrate (profileName: rec { + name = "clap_lex"; + version = "0.2.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213"; }; + dependencies = { + os_str_bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".os_str_bytes."6.0.1" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".cloudabi."0.0.3" = overridableMkRustCrate (profileName: rec { name = "cloudabi"; version = "0.0.3"; @@ -630,7 +690,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -728,6 +788,17 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".crypto-mac."0.11.1" = overridableMkRustCrate (profileName: rec { + name = "crypto-mac"; + version = "0.11.1"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714"; }; + dependencies = { + generic_array = rustPackages."registry+https://github.com/rust-lang/crates.io-index".generic-array."0.14.5" { inherit profileName; }; + subtle = rustPackages."registry+https://github.com/rust-lang/crates.io-index".subtle."2.4.1" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".ct-logs."0.8.0" = overridableMkRustCrate (profileName: rec { name = "ct-logs"; version = "0.8.0"; @@ -768,7 +839,7 @@ in proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; strsim = rustPackages."registry+https://github.com/rust-lang/crates.io-index".strsim."0.10.0" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; }); @@ -780,7 +851,7 @@ in dependencies = { darling_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".darling_core."0.13.1" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; }); @@ -806,7 +877,7 @@ in dependencies = { proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; }); @@ -920,7 +991,7 @@ in proc_macro_error = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro-error."1.0.4" { inherit profileName; }; proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; synstructure = rustPackages."registry+https://github.com/rust-lang/crates.io-index".synstructure."0.12.6" { inherit profileName; }; }; buildDependencies = { @@ -941,7 +1012,7 @@ in proc_macro_error = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro-error."1.0.4" { inherit profileName; }; proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; synstructure = rustPackages."registry+https://github.com/rust-lang/crates.io-index".synstructure."0.12.6" { inherit profileName; }; }; buildDependencies = { @@ -1112,7 +1183,7 @@ in dependencies = { proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; }); @@ -1212,7 +1283,7 @@ in pretty_env_logger = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pretty_env_logger."0.4.0" { inherit profileName; }; rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; serde_bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; sled = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; structopt = rustPackages."registry+https://github.com/rust-lang/crates.io-index".structopt."0.3.26" { inherit profileName; }; @@ -1228,7 +1299,7 @@ in hmac = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hmac."0.10.1" { inherit profileName; }; http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; hyper = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; - serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" { inherit profileName; }; + serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; sha2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sha2."0.9.9" { inherit profileName; }; static_init = rustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init."1.0.2" { inherit profileName; }; }; @@ -1292,9 +1363,9 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "pin_project" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "quick_xml" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quick-xml."0.21.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "roxmltree" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".roxmltree."0.14.1" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "serde_bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "serde_json" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "serde_json" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "sha2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sha2."0.9.9" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "tracing" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; @@ -1319,7 +1390,7 @@ in opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; serde_bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; sled = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; @@ -1346,7 +1417,7 @@ in netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.3.1" { inherit profileName; }; rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; serde_bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; sled = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; @@ -1380,7 +1451,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "serde_bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "sled" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; @@ -1409,9 +1480,9 @@ in netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.3.1" { inherit profileName; }; rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; serde_bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; - serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" { inherit profileName; }; + serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; tokio_stream = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.8" { inherit profileName; }; }; @@ -1450,9 +1521,9 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "schemars" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".schemars."0.8.8" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "serde_bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "serde_json" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "serde_json" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "tokio_stream" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.8" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "tracing" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; @@ -1475,7 +1546,7 @@ in log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; serde_bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; sled = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; @@ -1498,7 +1569,7 @@ in opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; serde_bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; sled = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; @@ -1523,8 +1594,8 @@ in netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.3.1" { inherit profileName; }; rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; - serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; sha2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sha2."0.9.9" { inherit profileName; }; sled = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; @@ -1553,8 +1624,8 @@ in opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; - serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; sha2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sha2."0.9.9" { inherit profileName; }; sled = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; @@ -1643,7 +1714,7 @@ in proc_macro_hack = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro-hack."0.5.19" { profileName = "__noProfile"; }; proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; }); @@ -1687,6 +1758,16 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".heck."0.4.0" = overridableMkRustCrate (profileName: rec { + name = "heck"; + version = "0.4.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"; }; + features = builtins.concatLists [ + [ "default" ] + ]; + }); + "registry+https://github.com/rust-lang/crates.io-index".hermit-abi."0.1.19" = overridableMkRustCrate (profileName: rec { name = "hermit-abi"; version = "0.1.19"; @@ -1734,6 +1815,17 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".hmac."0.11.0" = overridableMkRustCrate (profileName: rec { + name = "hmac"; + version = "0.11.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"; }; + dependencies = { + crypto_mac = rustPackages."registry+https://github.com/rust-lang/crates.io-index".crypto-mac."0.11.1" { inherit profileName; }; + digest = rustPackages."registry+https://github.com/rust-lang/crates.io-index".digest."0.9.0" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" = overridableMkRustCrate (profileName: rec { name = "http"; version = "0.2.6"; @@ -1806,31 +1898,31 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2"; }; features = builtins.concatLists [ - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "client") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "client") [ "default" ] (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "full") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "h2") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "http1") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "http2") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "h2") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "http1") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "http2") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "runtime") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "server") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "socket2") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "stream") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "tcp") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "socket2") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "stream") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "tcp") ]; dependencies = { bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; futures_channel = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-channel."0.3.21" { inherit profileName; }; futures_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "h2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".h2."0.3.12" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "h2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".h2."0.3.12" { inherit profileName; }; http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; http_body = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."0.4.4" { inherit profileName; }; httparse = rustPackages."registry+https://github.com/rust-lang/crates.io-index".httparse."1.6.0" { inherit profileName; }; httpdate = rustPackages."registry+https://github.com/rust-lang/crates.io-index".httpdate."1.0.2" { inherit profileName; }; itoa = rustPackages."registry+https://github.com/rust-lang/crates.io-index".itoa."1.0.1" { inherit profileName; }; pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "socket2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".socket2."0.4.4" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "socket2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".socket2."0.4.4" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; tower_service = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.1" { inherit profileName; }; tracing = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; @@ -1945,7 +2037,7 @@ in [ "serde" ] ]; dependencies = { - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; }; }); @@ -2009,8 +2101,8 @@ in [ "treediff" ] ]; dependencies = { - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; - serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; treediff = rustPackages."registry+https://github.com/rust-lang/crates.io-index".treediff."3.0.2" { inherit profileName; }; }; }); @@ -2022,8 +2114,33 @@ in src = fetchCratesIo { inherit name version; sha256 = "eaa63191d68230cccb81c5aa23abd53ed64d83337cacbb25a7b8c7979523774f"; }; dependencies = { log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; - serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; + }; + }); + + "unknown".k2v-client."0.1.0" = overridableMkRustCrate (profileName: rec { + name = "k2v-client"; + version = "0.1.0"; + registry = "unknown"; + src = fetchCrateLocal (workspaceSrc + "/src/k2v-client"); + features = builtins.concatLists [ + [ "clap" ] + [ "cli" ] + [ "garage_util" ] + ]; + dependencies = { + base64 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.13.0" { inherit profileName; }; + clap = rustPackages."registry+https://github.com/rust-lang/crates.io-index".clap."3.1.18" { inherit profileName; }; + garage_util = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; + http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; + rusoto_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rusoto_core."0.48.0" { inherit profileName; }; + rusoto_credential = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rusoto_credential."0.48.0" { inherit profileName; }; + rusoto_signature = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rusoto_signature."0.48.0" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; + thiserror = rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.31" { inherit profileName; }; + tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; }; }); @@ -2046,9 +2163,9 @@ in chrono = rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.19" { inherit profileName; }; http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; percent_encoding = rustPackages."registry+https://github.com/rust-lang/crates.io-index".percent-encoding."2.1.0" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; serde_value = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde-value."0.7.0" { inherit profileName; }; - serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" { inherit profileName; }; + serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; url = rustPackages."registry+https://github.com/rust-lang/crates.io-index".url."2.2.2" { inherit profileName; }; }; }); @@ -2129,10 +2246,10 @@ in openssl = rustPackages."registry+https://github.com/rust-lang/crates.io-index".openssl."0.10.38" { inherit profileName; }; pem = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pem."0.8.3" { inherit profileName; }; pin_project = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; - serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; serde_yaml = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_yaml."0.8.23" { inherit profileName; }; - thiserror = rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.30" { inherit profileName; }; + thiserror = rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.31" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; tokio_native_tls = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-native-tls."0.3.0" { inherit profileName; }; tokio_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.6.9" { inherit profileName; }; @@ -2158,9 +2275,9 @@ in json_patch = rustPackages."registry+https://github.com/rust-lang/crates.io-index".json-patch."0.2.6" { inherit profileName; }; k8s_openapi = rustPackages."registry+https://github.com/rust-lang/crates.io-index".k8s-openapi."0.13.1" { inherit profileName; }; once_cell = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; - serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" { inherit profileName; }; - thiserror = rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.30" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; + thiserror = rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.31" { inherit profileName; }; }; }); @@ -2177,8 +2294,8 @@ in darling = rustPackages."registry+https://github.com/rust-lang/crates.io-index".darling."0.13.1" { inherit profileName; }; proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; }); @@ -2195,8 +2312,8 @@ in k8s_openapi = rustPackages."registry+https://github.com/rust-lang/crates.io-index".k8s-openapi."0.13.1" { inherit profileName; }; kube_client = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kube-client."0.62.0" { inherit profileName; }; pin_project = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; - serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; smallvec = rustPackages."registry+https://github.com/rust-lang/crates.io-index".smallvec."1.8.0" { inherit profileName; }; snafu = rustPackages."registry+https://github.com/rust-lang/crates.io-index".snafu."0.6.10" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; @@ -2220,7 +2337,7 @@ in hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; sodiumoxide = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; - thiserror = rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.30" { inherit profileName; }; + thiserror = rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.31" { inherit profileName; }; }; }); @@ -2237,7 +2354,7 @@ in dependencies = { libc = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; libsodium_sys = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libsodium-sys."0.2.7" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; }; }); @@ -2480,7 +2597,7 @@ in sodiumoxide = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.14.4" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; tokio_stream = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.8" { inherit profileName; }; tokio_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.6.9" { inherit profileName; }; @@ -2514,7 +2631,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "opentelemetry_contrib" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-contrib."0.9.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.5.6" { inherit profileName; }; rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.14.4" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; tokio_stream = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.8" { inherit profileName; }; tokio_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.6.9" { inherit profileName; }; @@ -2629,15 +2746,15 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95"; }; features = builtins.concatLists [ - [ "vendored" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "vendored") ]; dependencies = { - bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; - cfg_if = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }; - foreign_types = rustPackages."registry+https://github.com/rust-lang/crates.io-index".foreign-types."0.3.2" { inherit profileName; }; - libc = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - once_cell = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; - ffi = rustPackages."registry+https://github.com/rust-lang/crates.io-index".openssl-sys."0.9.72" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client" then "bitflags" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client" then "cfg_if" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client" then "foreign_types" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".foreign-types."0.3.2" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client" then "ffi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".openssl-sys."0.9.72" { inherit profileName; }; }; }); @@ -2667,18 +2784,18 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb"; }; features = builtins.concatLists [ - [ "openssl-src" ] - [ "vendored" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "openssl-src") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "vendored") ]; dependencies = { - libc = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; buildDependencies = { - autocfg = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".autocfg."1.1.0" { profileName = "__noProfile"; }; - cc = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".cc."1.0.73" { profileName = "__noProfile"; }; - openssl_src = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".openssl-src."111.18.0+1.1.1n" { profileName = "__noProfile"; }; - pkg_config = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".pkg-config."0.3.24" { profileName = "__noProfile"; }; - ${ if hostPlatform.parsed.abi.name == "msvc" then "vcpkg" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".vcpkg."0.2.15" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client" then "autocfg" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".autocfg."1.1.0" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client" then "cc" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".cc."1.0.73" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "openssl_src" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".openssl-src."111.18.0+1.1.1n" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client" then "pkg_config" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".pkg-config."0.3.24" { profileName = "__noProfile"; }; + ${ if (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") && hostPlatform.parsed.abi.name == "msvc" then "vcpkg" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".vcpkg."0.2.15" { profileName = "__noProfile"; }; }; }); @@ -2715,7 +2832,7 @@ in percent_encoding = rustPackages."registry+https://github.com/rust-lang/crates.io-index".percent-encoding."2.1.0" { inherit profileName; }; pin_project = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; }; rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; - thiserror = rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.30" { inherit profileName; }; + thiserror = rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.31" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; tokio_stream = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.8" { inherit profileName; }; }; @@ -2754,7 +2871,7 @@ in http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; prost = rustPackages."registry+https://github.com/rust-lang/crates.io-index".prost."0.9.0" { inherit profileName; }; - thiserror = rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.30" { inherit profileName; }; + thiserror = rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.31" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; tonic = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tonic."0.6.2" { inherit profileName; }; }; @@ -2789,6 +2906,16 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".os_str_bytes."6.0.1" = overridableMkRustCrate (profileName: rec { + name = "os_str_bytes"; + version = "6.0.1"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "029d8d0b2f198229de29dca79676f2738ff952edf3fde542eb8bf94d8c21b435"; }; + features = builtins.concatLists [ + [ "raw_os_str" ] + ]; + }); + "registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" = overridableMkRustCrate (profileName: rec { name = "parking_lot"; version = "0.11.2"; @@ -2905,7 +3032,7 @@ in dependencies = { proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; }); @@ -2917,7 +3044,7 @@ in dependencies = { proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; }); @@ -3010,7 +3137,7 @@ in proc_macro_error_attr = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro-error-attr."1.0.4" { profileName = "__noProfile"; }; proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; buildDependencies = { version_check = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".version_check."0.9.4" { profileName = "__noProfile"; }; @@ -3068,7 +3195,7 @@ in memchr = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; }; parking_lot = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; }; protobuf = rustPackages."registry+https://github.com/rust-lang/crates.io-index".protobuf."2.27.1" { inherit profileName; }; - thiserror = rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.30" { inherit profileName; }; + thiserror = rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.31" { inherit profileName; }; }; }); @@ -3121,7 +3248,7 @@ in itertools = rustPackages."registry+https://github.com/rust-lang/crates.io-index".itertools."0.10.3" { inherit profileName; }; proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; }); @@ -3162,7 +3289,7 @@ in ]; dependencies = { memchr = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; }; }); @@ -3297,7 +3424,7 @@ in dependencies = { getrandom = rustPackages."registry+https://github.com/rust-lang/crates.io-index".getrandom."0.2.5" { inherit profileName; }; syscall = rustPackages."registry+https://github.com/rust-lang/crates.io-index".redox_syscall."0.2.11" { inherit profileName; }; - thiserror = rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.30" { inherit profileName; }; + thiserror = rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.31" { inherit profileName; }; }; }); @@ -3373,7 +3500,7 @@ in ]; dependencies = { ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" || hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "i686" || hostPlatform.parsed.cpu.name == "x86_64" || (hostPlatform.parsed.cpu.name == "aarch64" || hostPlatform.parsed.cpu.name == "armv6l" || hostPlatform.parsed.cpu.name == "armv7l") && (hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "fuchsia" || hostPlatform.parsed.kernel.name == "linux") then "spin" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".spin."0.5.2" { inherit profileName; }; untrusted = rustPackages."registry+https://github.com/rust-lang/crates.io-index".untrusted."0.7.1" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "wasm32" && hostPlatform.parsed.vendor.name == "unknown" && hostPlatform.parsed.kernel.name == "unknown" && hostPlatform.parsed.abi.name == "" then "web_sys" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.56" { inherit profileName; }; @@ -3403,7 +3530,7 @@ in dependencies = { byteorder = rustPackages."registry+https://github.com/rust-lang/crates.io-index".byteorder."1.4.3" { inherit profileName; }; rmp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp."0.8.10" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; }; }); @@ -3415,7 +3542,7 @@ in dependencies = { byteorder = rustPackages."registry+https://github.com/rust-lang/crates.io-index".byteorder."1.4.3" { inherit profileName; }; rmp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp."0.8.10" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; }; }); @@ -3433,6 +3560,87 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".rusoto_core."0.48.0" = overridableMkRustCrate (profileName: rec { + name = "rusoto_core"; + version = "0.48.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "1db30db44ea73551326269adcf7a2169428a054f14faf9e1768f2163494f2fa2"; }; + features = builtins.concatLists [ + [ "default" ] + [ "hyper-tls" ] + [ "native-tls" ] + ]; + dependencies = { + async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; + base64 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.13.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + crc32fast = rustPackages."registry+https://github.com/rust-lang/crates.io-index".crc32fast."1.3.2" { inherit profileName; }; + futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; + http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; + hyper = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; + hyper_tls = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper-tls."0.5.0" { inherit profileName; }; + lazy_static = rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { inherit profileName; }; + log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; + rusoto_credential = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rusoto_credential."0.48.0" { inherit profileName; }; + rusoto_signature = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rusoto_signature."0.48.0" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; + tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; + xml = rustPackages."registry+https://github.com/rust-lang/crates.io-index".xml-rs."0.8.4" { inherit profileName; }; + }; + buildDependencies = { + rustc_version = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".rustc_version."0.4.0" { profileName = "__noProfile"; }; + }; + }); + + "registry+https://github.com/rust-lang/crates.io-index".rusoto_credential."0.48.0" = overridableMkRustCrate (profileName: rec { + name = "rusoto_credential"; + version = "0.48.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "ee0a6c13db5aad6047b6a44ef023dbbc21a056b6dab5be3b79ce4283d5c02d05"; }; + dependencies = { + async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; + chrono = rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.19" { inherit profileName; }; + dirs_next = rustPackages."registry+https://github.com/rust-lang/crates.io-index".dirs-next."2.0.0" { inherit profileName; }; + futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; + hyper = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; + shlex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".shlex."1.1.0" { inherit profileName; }; + tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; + zeroize = rustPackages."registry+https://github.com/rust-lang/crates.io-index".zeroize."1.5.4" { inherit profileName; }; + }; + }); + + "registry+https://github.com/rust-lang/crates.io-index".rusoto_signature."0.48.0" = overridableMkRustCrate (profileName: rec { + name = "rusoto_signature"; + version = "0.48.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "a5ae95491c8b4847931e291b151127eccd6ff8ca13f33603eb3d0035ecb05272"; }; + dependencies = { + base64 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.13.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + chrono = rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.19" { inherit profileName; }; + digest = rustPackages."registry+https://github.com/rust-lang/crates.io-index".digest."0.9.0" { inherit profileName; }; + futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; + hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; + hmac = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hmac."0.11.0" { inherit profileName; }; + http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; + hyper = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; + log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; + md5 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".md-5."0.9.1" { inherit profileName; }; + percent_encoding = rustPackages."registry+https://github.com/rust-lang/crates.io-index".percent-encoding."2.1.0" { inherit profileName; }; + pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; + rusoto_credential = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rusoto_credential."0.48.0" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + sha2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sha2."0.9.9" { inherit profileName; }; + tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; + }; + buildDependencies = { + rustc_version = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".rustc_version."0.4.0" { profileName = "__noProfile"; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".rustc_version."0.4.0" = overridableMkRustCrate (profileName: rec { name = "rustc_version"; version = "0.4.0"; @@ -3527,8 +3735,8 @@ in dependencies = { dyn_clone = rustPackages."registry+https://github.com/rust-lang/crates.io-index".dyn-clone."1.0.5" { inherit profileName; }; schemars_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".schemars_derive."0.8.8" { profileName = "__noProfile"; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; - serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; }; }); @@ -3541,7 +3749,7 @@ in proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; serde_derive_internals = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_derive_internals."0.25.0" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; }); @@ -3607,11 +3815,11 @@ in ]; }); - "registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" = overridableMkRustCrate (profileName: rec { + "registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" = overridableMkRustCrate (profileName: rec { name = "serde"; - version = "1.0.136"; + version = "1.0.137"; registry = "registry+https://github.com/rust-lang/crates.io-index"; - src = fetchCratesIo { inherit name version; sha256 = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"; }; + src = fetchCratesIo { inherit name version; sha256 = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"; }; features = builtins.concatLists [ [ "default" ] [ "derive" ] @@ -3620,7 +3828,7 @@ in [ "std" ] ]; dependencies = { - serde_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_derive."1.0.136" { profileName = "__noProfile"; }; + serde_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_derive."1.0.137" { profileName = "__noProfile"; }; }; }); @@ -3631,7 +3839,7 @@ in src = fetchCratesIo { inherit name version; sha256 = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"; }; dependencies = { ordered_float = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ordered-float."2.10.0" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; }; }); @@ -3645,22 +3853,22 @@ in [ "std" ] ]; dependencies = { - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; }; }); - "registry+https://github.com/rust-lang/crates.io-index".serde_derive."1.0.136" = overridableMkRustCrate (profileName: rec { + "registry+https://github.com/rust-lang/crates.io-index".serde_derive."1.0.137" = overridableMkRustCrate (profileName: rec { name = "serde_derive"; - version = "1.0.136"; + version = "1.0.137"; registry = "registry+https://github.com/rust-lang/crates.io-index"; - src = fetchCratesIo { inherit name version; sha256 = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"; }; + src = fetchCratesIo { inherit name version; sha256 = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"; }; features = builtins.concatLists [ [ "default" ] ]; dependencies = { proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; }); @@ -3672,15 +3880,15 @@ in dependencies = { proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; }); - "registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" = overridableMkRustCrate (profileName: rec { + "registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" = overridableMkRustCrate (profileName: rec { name = "serde_json"; - version = "1.0.79"; + version = "1.0.81"; registry = "registry+https://github.com/rust-lang/crates.io-index"; - src = fetchCratesIo { inherit name version; sha256 = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"; }; + src = fetchCratesIo { inherit name version; sha256 = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"; }; features = builtins.concatLists [ [ "default" ] (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "indexmap") @@ -3691,7 +3899,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "indexmap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".indexmap."1.8.0" { inherit profileName; }; itoa = rustPackages."registry+https://github.com/rust-lang/crates.io-index".itoa."1.0.1" { inherit profileName; }; ryu = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ryu."1.0.9" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; }; }); @@ -3703,7 +3911,7 @@ in dependencies = { indexmap = rustPackages."registry+https://github.com/rust-lang/crates.io-index".indexmap."1.8.0" { inherit profileName; }; ryu = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ryu."1.0.9" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; yaml_rust = rustPackages."registry+https://github.com/rust-lang/crates.io-index".yaml-rust."0.4.5" { inherit profileName; }; }; }); @@ -3726,6 +3934,17 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".shlex."1.1.0" = overridableMkRustCrate (profileName: rec { + name = "shlex"; + version = "1.1.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"; }; + features = builtins.concatLists [ + [ "default" ] + [ "std" ] + ]; + }); + "registry+https://github.com/rust-lang/crates.io-index".signal-hook-registry."1.4.0" = overridableMkRustCrate (profileName: rec { name = "signal-hook-registry"; version = "1.4.0"; @@ -3804,7 +4023,7 @@ in dependencies = { proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; }); @@ -3850,7 +4069,7 @@ in ]; dependencies = { bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot_core."0.8.5" { inherit profileName; }; static_init_macro = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init_macro."1.0.2" { profileName = "__noProfile"; }; @@ -3870,7 +4089,7 @@ in memchr = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; }; proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; buildDependencies = { cfg_aliases = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg_aliases."0.1.1" { profileName = "__noProfile"; }; @@ -3906,7 +4125,7 @@ in proc_macro_error = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro-error."1.0.4" { inherit profileName; }; proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; }); @@ -3917,11 +4136,11 @@ in src = fetchCratesIo { inherit name version; sha256 = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"; }; }); - "registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" = overridableMkRustCrate (profileName: rec { + "registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" = overridableMkRustCrate (profileName: rec { name = "syn"; - version = "1.0.89"; + version = "1.0.94"; registry = "registry+https://github.com/rust-lang/crates.io-index"; - src = fetchCratesIo { inherit name version; sha256 = "ea297be220d52398dcc07ce15a209fce436d361735ac1db700cab3b6cdfb9f54"; }; + src = fetchCratesIo { inherit name version; sha256 = "a07e33e919ebcd69113d5be0e4d70c5707004ff45188910106854f38b960df4a"; }; features = builtins.concatLists [ [ "clone-impls" ] [ "default" ] @@ -3954,7 +4173,7 @@ in dependencies = { proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; unicode_xid = rustPackages."registry+https://github.com/rust-lang/crates.io-index".unicode-xid."0.2.2" { inherit profileName; }; }; }); @@ -3994,25 +4213,32 @@ in }; }); - "registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.30" = overridableMkRustCrate (profileName: rec { - name = "thiserror"; - version = "1.0.30"; + "registry+https://github.com/rust-lang/crates.io-index".textwrap."0.15.0" = overridableMkRustCrate (profileName: rec { + name = "textwrap"; + version = "0.15.0"; registry = "registry+https://github.com/rust-lang/crates.io-index"; - src = fetchCratesIo { inherit name version; sha256 = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"; }; + src = fetchCratesIo { inherit name version; sha256 = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"; }; + }); + + "registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.31" = overridableMkRustCrate (profileName: rec { + name = "thiserror"; + version = "1.0.31"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"; }; dependencies = { - thiserror_impl = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror-impl."1.0.30" { profileName = "__noProfile"; }; + thiserror_impl = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror-impl."1.0.31" { profileName = "__noProfile"; }; }; }); - "registry+https://github.com/rust-lang/crates.io-index".thiserror-impl."1.0.30" = overridableMkRustCrate (profileName: rec { + "registry+https://github.com/rust-lang/crates.io-index".thiserror-impl."1.0.31" = overridableMkRustCrate (profileName: rec { name = "thiserror-impl"; - version = "1.0.30"; + version = "1.0.31"; registry = "registry+https://github.com/rust-lang/crates.io-index"; - src = fetchCratesIo { inherit name version; sha256 = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"; }; + src = fetchCratesIo { inherit name version; sha256 = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"; }; dependencies = { proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; }); @@ -4077,7 +4303,7 @@ in [ "default" ] [ "fs" ] (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "full") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "io-std") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "io-std") [ "io-util" ] [ "libc" ] [ "macros" ] @@ -4087,7 +4313,7 @@ in [ "num_cpus" ] [ "once_cell" ] (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "parking_lot") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "process") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "process") [ "rt" ] [ "rt-multi-thread" ] [ "signal" ] @@ -4133,7 +4359,7 @@ in dependencies = { proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; }); @@ -4183,9 +4409,9 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0"; }; features = builtins.concatLists [ - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "codec") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "codec") [ "compat" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "default") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") [ "futures-io" ] (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "io") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "slab") @@ -4227,7 +4453,7 @@ in [ "default" ] ]; dependencies = { - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.136" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; }; }); @@ -4292,7 +4518,7 @@ in proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; prost_build = rustPackages."registry+https://github.com/rust-lang/crates.io-index".prost-build."0.9.0" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; }); @@ -4413,7 +4639,7 @@ in dependencies = { proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; }; }); @@ -4458,7 +4684,7 @@ in [ "with-serde-json" ] ]; dependencies = { - serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.79" { inherit profileName; }; + serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; }; }); @@ -4637,7 +4863,7 @@ in log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; wasm_bindgen_shared = rustPackages."registry+https://github.com/rust-lang/crates.io-index".wasm-bindgen-shared."0.2.79" { inherit profileName; }; }; }); @@ -4667,7 +4893,7 @@ in dependencies = { proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; quote = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.16" { inherit profileName; }; - syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.89" { inherit profileName; }; + syn = rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }; wasm_bindgen_backend = rustPackages."registry+https://github.com/rust-lang/crates.io-index".wasm-bindgen-backend."0.2.79" { inherit profileName; }; wasm_bindgen_shared = rustPackages."registry+https://github.com/rust-lang/crates.io-index".wasm-bindgen-shared."0.2.79" { inherit profileName; }; }; @@ -4739,9 +4965,9 @@ in [ "in6addr" ] [ "inaddr" ] [ "ioapiset" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "knownfolders") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "lmcons") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "minschannel") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "knownfolders") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "lmcons") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "minschannel") [ "minwinbase" ] [ "minwindef" ] [ "mswsock" ] @@ -4749,22 +4975,22 @@ in [ "ntdef" ] [ "ntsecapi" ] [ "ntstatus" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "objbase") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "objbase") [ "processenv" ] [ "processthreadsapi" ] [ "profileapi" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "schannel") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "securitybaseapi") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "shlobj") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "sspi") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "schannel") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "securitybaseapi") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "shlobj") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "sspi") [ "std" ] [ "synchapi" ] [ "sysinfoapi" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "threadpoollegacyapiset") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "threadpoollegacyapiset") [ "timezoneapi" ] [ "winbase" ] [ "wincon" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "wincrypt") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "wincrypt") [ "windef" ] [ "winerror" ] [ "winioctl" ] @@ -4820,11 +5046,11 @@ in [ "default" ] ]; dependencies = { - ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-uwp-windows-gnu" || hostPlatform.config == "i686-pc-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); @@ -4863,6 +5089,13 @@ in src = fetchCratesIo { inherit name version; sha256 = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316"; }; }); + "registry+https://github.com/rust-lang/crates.io-index".xml-rs."0.8.4" = overridableMkRustCrate (profileName: rec { + name = "xml-rs"; + version = "0.8.4"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"; }; + }); + "registry+https://github.com/rust-lang/crates.io-index".xmlparser."0.13.3" = overridableMkRustCrate (profileName: rec { name = "xmlparser"; version = "0.13.3"; diff --git a/Cargo.toml b/Cargo.toml index cfc48113..c64f7897 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,9 +8,12 @@ members = [ "src/admin", "src/api", "src/web", - "src/garage" + "src/garage", + "src/k2v-client", ] +default-members = ["src/garage"] + [profile.dev] lto = "off" diff --git a/shell.nix b/shell.nix index 867d7f48..13ea4a0e 100644 --- a/shell.nix +++ b/shell.nix @@ -79,6 +79,8 @@ function refresh_toolchain { pkgs.rustfmt pkgs.perl pkgs.protobuf + pkgs.pkg-config + pkgs.openssl cargo2nix.packages.x86_64-linux.cargo2nix ] else []) ++ diff --git a/src/garage/cli/cmd.rs b/src/garage/cli/cmd.rs index 2a799868..b2dd8f14 100644 --- a/src/garage/cli/cmd.rs +++ b/src/garage/cli/cmd.rs @@ -1,6 +1,7 @@ use std::collections::HashSet; use garage_util::error::*; +use garage_util::formater::format_table; use garage_rpc::layout::*; use garage_rpc::system::*; diff --git a/src/garage/cli/layout.rs b/src/garage/cli/layout.rs index 88941d78..0247c32b 100644 --- a/src/garage/cli/layout.rs +++ b/src/garage/cli/layout.rs @@ -1,6 +1,7 @@ use garage_util::crdt::Crdt; use garage_util::data::*; use garage_util::error::*; +use garage_util::formater::format_table; use garage_rpc::layout::*; use garage_rpc::system::*; diff --git a/src/garage/cli/util.rs b/src/garage/cli/util.rs index fe11ad44..6d73be3a 100644 --- a/src/garage/cli/util.rs +++ b/src/garage/cli/util.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use garage_util::crdt::*; use garage_util::data::Uuid; use garage_util::error::*; +use garage_util::formater::format_table; use garage_model::bucket_table::*; use garage_model::key_table::*; @@ -173,35 +174,6 @@ pub fn print_bucket_info(bucket: &Bucket, relevant_keys: &HashMap) }; } -pub fn format_table(data: Vec) { - let data = data - .iter() - .map(|s| s.split('\t').collect::>()) - .collect::>(); - - let columns = data.iter().map(|row| row.len()).fold(0, std::cmp::max); - let mut column_size = vec![0; columns]; - - let mut out = String::new(); - - for row in data.iter() { - for (i, col) in row.iter().enumerate() { - column_size[i] = std::cmp::max(column_size[i], col.chars().count()); - } - } - - for row in data.iter() { - for (col, col_len) in row[..row.len() - 1].iter().zip(column_size.iter()) { - out.push_str(col); - (0..col_len - col.chars().count() + 2).for_each(|_| out.push(' ')); - } - out.push_str(row[row.len() - 1]); - out.push('\n'); - } - - print!("{}", out); -} - pub fn find_matching_node( cand: impl std::iter::Iterator, pattern: &str, diff --git a/src/k2v-client/Cargo.toml b/src/k2v-client/Cargo.toml new file mode 100644 index 00000000..84c6b8b2 --- /dev/null +++ b/src/k2v-client/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "k2v-client" +version = "0.1.0" +edition = "2018" + +[dependencies] +base64 = "0.13.0" +http = "0.2.6" +rusoto_core = "0.48.0" +rusoto_credential = "0.48.0" +rusoto_signature = "0.48.0" +serde = "1.0.137" +serde_json = "1.0.81" +thiserror = "1.0.31" +tokio = "1.17.0" + +# cli deps +clap = { version = "3.1.18", optional = true, features = ["derive", "env"] } +garage_util = { path = "../util", optional = true } + + +[features] +cli = ["clap", "tokio/fs", "tokio/io-std", "garage_util"] + +[[bin]] +name = "k2v-cli" +required-features = ["cli"] diff --git a/src/k2v-client/README.md b/src/k2v-client/README.md new file mode 100644 index 00000000..db454805 --- /dev/null +++ b/src/k2v-client/README.md @@ -0,0 +1,25 @@ +Example usage: +```sh +# all these values can be provided on the cli instead +export AWS_ACCESS_KEY_ID=GK123456 +export AWS_SECRET_ACCESS_KEY=0123..789 +export AWS_REGION=garage +export K2V_ENDPOINT=http://172.30.2.1:3903 +export K2V_BUCKET=my-bucket + +cargo run --features=cli -- read-range my-partition-key --all + +cargo run --features=cli -- insert my-partition-key my-sort-key --text "my string1" +cargo run --features=cli -- insert my-partition-key my-sort-key --text "my string2" +cargo run --features=cli -- insert my-partition-key my-sort-key2 --text "my string" + +cargo run --features=cli -- read-range my-partition-key --all + +causality=$(cargo run --features=cli -- read my-partition-key my-sort-key2 -b | head -n1) +cargo run --features=cli -- delete my-partition-key my-sort-key2 -c $causality + +causality=$(cargo run --features=cli -- read my-partition-key my-sort-key -b | head -n1) +cargo run --features=cli -- insert my-partition-key my-sort-key --text "my string3" -c $causality + +cargo run --features=cli -- read-range my-partition-key --all +``` diff --git a/src/k2v-client/src/bin/k2v-cli.rs b/src/k2v-client/src/bin/k2v-cli.rs new file mode 100644 index 00000000..38c39361 --- /dev/null +++ b/src/k2v-client/src/bin/k2v-cli.rs @@ -0,0 +1,466 @@ +use k2v_client::*; + +use garage_util::formater::format_table; + +use rusoto_core::credential::AwsCredentials; +use rusoto_core::Region; + +use clap::{Parser, Subcommand}; + +/// K2V command line interface +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Args { + /// Name of the region to use + #[clap(short, long, env = "AWS_REGION", default_value = "garage")] + region: String, + /// Url of the endpoint to connect to + #[clap(short, long, env = "K2V_ENDPOINT")] + endpoint: String, + /// Access key ID + #[clap(short, long, env = "AWS_ACCESS_KEY_ID")] + key_id: String, + /// Access key ID + #[clap(short, long, env = "AWS_SECRET_ACCESS_KEY")] + secret: String, + /// Bucket name + #[clap(short, long, env = "K2V_BUCKET")] + bucket: String, + #[clap(subcommand)] + command: Command, +} + +#[derive(Subcommand, Debug)] +enum Command { + /// Insert a single value + Insert { + /// Partition key to insert to + partition_key: String, + /// Sort key to insert to + sort_key: String, + /// Causality of the insertion + #[clap(short, long)] + causality: Option, + /// Value to insert + #[clap(flatten)] + value: Value, + }, + /// Read a single value + Read { + /// Partition key to read from + partition_key: String, + /// Sort key to read from + sort_key: String, + /// Output formating + #[clap(flatten)] + output_kind: ReadOutputKind, + }, + /// Delete a single value + Delete { + /// Partition key to delete from + partition_key: String, + /// Sort key to delete from + sort_key: String, + /// Causality information + #[clap(short, long)] + causality: String, + }, + /// List partition keys + ReadIndex { + /// Output formating + #[clap(flatten)] + output_kind: BatchOutputKind, + /// Output only partition keys matching this filter + #[clap(flatten)] + filter: Filter, + }, + /// Read a range of sort keys + ReadRange { + /// Partition key to read from + partition_key: String, + /// Output formating + #[clap(flatten)] + output_kind: BatchOutputKind, + /// Output only sort keys matching this filter + #[clap(flatten)] + filter: Filter, + }, + /// Delete a range of sort keys + DeleteRange { + /// Partition key to delete from + partition_key: String, + /// Output formating + #[clap(flatten)] + output_kind: BatchOutputKind, + /// Delete only sort keys matching this filter + #[clap(flatten)] + filter: Filter, + }, +} + +/// Where to read a value from +#[derive(Parser, Debug)] +#[clap(group = clap::ArgGroup::new("value").multiple(false).required(true))] +struct Value { + /// Read value from a file. use - to read from stdin + #[clap(short, long, group = "value")] + file: Option, + /// Read a base64 value from commandline + #[clap(short, long, group = "value")] + b64: Option, + /// Read a raw (UTF-8) value from the commandline + #[clap(short, long, group = "value")] + text: Option, +} + +impl Value { + async fn to_data(&self) -> Result, Error> { + if let Some(ref text) = self.text { + Ok(text.as_bytes().to_vec()) + } else if let Some(ref b64) = self.b64 { + base64::decode(b64).map_err(|_| Error::Message("invalid base64 input".into())) + } else if let Some(ref path) = self.file { + use tokio::io::AsyncReadExt; + if path == "-" { + let mut file = tokio::io::stdin(); + let mut vec = Vec::new(); + file.read_to_end(&mut vec).await?; + Ok(vec) + } else { + let mut file = tokio::fs::File::open(path).await?; + let mut vec = Vec::new(); + file.read_to_end(&mut vec).await?; + Ok(vec) + } + } else { + unreachable!("Value must have one option set") + } + } +} + +#[derive(Parser, Debug)] +#[clap(group = clap::ArgGroup::new("output-kind").multiple(false).required(false))] +struct ReadOutputKind { + /// Base64 output. Conflicts are line separated, first line is causality token + #[clap(short, long, group = "output-kind")] + b64: bool, + /// Raw output. Conflicts generate error, causality token is not returned + #[clap(short, long, group = "output-kind")] + raw: bool, + /// Human formated output + #[clap(short = 'H', long, group = "output-kind")] + human: bool, + /// JSON formated output + #[clap(short, long, group = "output-kind")] + json: bool, +} + +impl ReadOutputKind { + fn display_output(&self, val: CausalValue) -> ! { + use std::io::Write; + use std::process::exit; + + if self.json { + let stdout = std::io::stdout(); + serde_json::to_writer_pretty(stdout, &val).unwrap(); + exit(0); + } + + if self.raw { + let mut val = val.value; + if val.len() != 1 { + eprintln!( + "Raw mode can only read non-concurent values, found {} values, expected 1", + val.len() + ); + exit(1); + } + let val = val.pop().unwrap(); + match val { + K2vValue::Value(v) => { + std::io::stdout().write_all(&v).unwrap(); + exit(0); + } + K2vValue::Tombstone => { + eprintln!("Expected value, found tombstone"); + exit(2); + } + } + } + + let causality: String = val.causality.into(); + if self.b64 { + println!("{}", causality); + for val in val.value { + match val { + K2vValue::Value(v) => { + println!("{}", base64::encode(&v)) + } + K2vValue::Tombstone => { + println!(); + } + } + } + exit(0); + } + + // human + println!("causality: {}", causality); + println!("values:"); + for val in val.value { + match val { + K2vValue::Value(v) => { + if let Ok(string) = std::str::from_utf8(&v) { + println!(" utf-8: {}", string); + } else { + println!(" base64: {}", base64::encode(&v)); + } + } + K2vValue::Tombstone => { + println!(" tombstone"); + } + } + } + exit(0); + } +} + +#[derive(Parser, Debug)] +#[clap(group = clap::ArgGroup::new("output-kind").multiple(false).required(false))] +struct BatchOutputKind { + /// Human formated output + #[clap(short = 'H', long, group = "output-kind")] + human: bool, + /// JSON formated output + #[clap(short, long, group = "output-kind")] + json: bool, +} + +/// Filter for batch operations +#[derive(Parser, Debug)] +#[clap(group = clap::ArgGroup::new("filter").multiple(true).required(true))] +struct Filter { + /// Match only keys starting with this prefix + #[clap(short, long, group = "filter")] + prefix: Option, + /// Match only keys lexicographically after this key (including this key itself) + #[clap(short, long, group = "filter")] + start: Option, + /// Match only keys lexicographically before this key (excluding this key) + #[clap(short, long, group = "filter")] + end: Option, + /// Only match the first X keys + #[clap(short, long)] + limit: Option, + /// Return keys in reverse order + #[clap(short, long)] + reverse: bool, + /// Return only keys where conflict happened + #[clap(short, long)] + conflicts_only: bool, + /// Also include keys storing only tombstones + #[clap(short, long)] + tombstones: bool, + /// Return any key + #[clap(short, long, group = "filter")] + all: bool, +} + +impl Filter { + fn k2v_filter(&self) -> k2v_client::Filter<'_> { + k2v_client::Filter { + start: self.start.as_deref(), + end: self.end.as_deref(), + prefix: self.prefix.as_deref(), + limit: self.limit, + reverse: self.reverse, + } + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + let args = Args::parse(); + + let region = Region::Custom { + name: args.region, + endpoint: args.endpoint, + }; + + let creds = AwsCredentials::new(args.key_id, args.secret, None, None); + + let client = K2vClient::new(region, args.bucket, creds, None)?; + + match args.command { + Command::Insert { + partition_key, + sort_key, + causality, + value, + } => { + client + .insert_item( + &partition_key, + &sort_key, + value.to_data().await?, + causality.map(Into::into), + ) + .await?; + } + Command::Delete { + partition_key, + sort_key, + causality, + } => { + client + .delete_item(&partition_key, &sort_key, causality.into()) + .await?; + } + Command::Read { + partition_key, + sort_key, + output_kind, + } => { + let res = client.read_item(&partition_key, &sort_key).await?; + output_kind.display_output(res); + } + Command::ReadIndex { + output_kind, + filter, + } => { + if filter.conflicts_only || filter.tombstones { + return Err(Error::Message( + "conlicts-only and tombstones are invalid for read-index".into(), + )); + } + let res = client.read_index(filter.k2v_filter()).await?; + if output_kind.json { + let values = res + .items + .into_iter() + .map(|(k, v)| { + let mut value = serde_json::to_value(v).unwrap(); + value + .as_object_mut() + .unwrap() + .insert("sort_key".to_owned(), k.into()); + value + }) + .collect::>(); + let json = serde_json::json!({ + "next_key": res.next_start, + "values": values, + }); + + let stdout = std::io::stdout(); + serde_json::to_writer_pretty(stdout, &json).unwrap(); + } else { + if let Some(next) = res.next_start { + println!("next key: {}", next); + } + + let mut to_print = Vec::new(); + to_print.push(format!("key:\tentries\tconflicts\tvalues\tbytes")); + for (k, v) in res.items { + to_print.push(format!( + "{}\t{}\t{}\t{}\t{}", + k, v.entries, v.conflicts, v.values, v.bytes + )); + } + format_table(to_print); + } + } + Command::ReadRange { + partition_key, + output_kind, + filter, + } => { + let op = BatchReadOp { + partition_key: &partition_key, + filter: filter.k2v_filter(), + conflicts_only: filter.conflicts_only, + tombstones: filter.tombstones, + single_item: false, + }; + let mut res = client.read_batch(&[op]).await?; + let res = res.pop().unwrap(); + if output_kind.json { + let values = res + .items + .into_iter() + .map(|(k, v)| { + let mut value = serde_json::to_value(v).unwrap(); + value + .as_object_mut() + .unwrap() + .insert("sort_key".to_owned(), k.into()); + value + }) + .collect::>(); + let json = serde_json::json!({ + "next_key": res.next_start, + "values": values, + }); + + let stdout = std::io::stdout(); + serde_json::to_writer_pretty(stdout, &json).unwrap(); + } else { + if let Some(next) = res.next_start { + println!("next key: {}", next); + } + for (key, values) in res.items { + println!("key: {}", key); + let causality: String = values.causality.into(); + println!("causality: {}", causality); + for value in values.value { + match value { + K2vValue::Value(v) => { + if let Ok(string) = std::str::from_utf8(&v) { + println!(" value(utf-8): {}", string); + } else { + println!(" value(base64): {}", base64::encode(&v)); + } + } + K2vValue::Tombstone => { + println!(" tombstone"); + } + } + } + } + } + } + Command::DeleteRange { + partition_key, + output_kind, + filter, + } => { + let op = BatchDeleteOp { + partition_key: &partition_key, + prefix: filter.prefix.as_deref(), + start: filter.start.as_deref(), + end: filter.end.as_deref(), + single_item: false, + }; + if filter.reverse + || filter.conflicts_only + || filter.tombstones + || filter.limit.is_some() + { + return Err(Error::Message( + "limit, conlicts-only, reverse and tombstones are invalid for delete-range" + .into(), + )); + } + + let res = client.delete_batch(&[op]).await?; + + if output_kind.json { + println!("{}", res[0]); + } else { + println!("deleted {} keys", res[0]); + } + } + } + + Ok(()) +} diff --git a/src/k2v-client/src/error.rs b/src/k2v-client/src/error.rs new file mode 100644 index 00000000..62357934 --- /dev/null +++ b/src/k2v-client/src/error.rs @@ -0,0 +1,22 @@ +use std::borrow::Cow; + +use thiserror::Error; + +/// Errors returned by this crate +#[derive(Error, Debug)] +pub enum Error { + #[error("received invalid response: {0}")] + InvalidResponse(Cow<'static, str>), + #[error("not found")] + NotFound, + #[error("io error: {0}")] + IoError(#[from] std::io::Error), + #[error("rusoto tls error: {0}")] + RusotoTls(#[from] rusoto_core::request::TlsError), + #[error("rusoto http error: {0}")] + RusotoHttp(#[from] rusoto_core::HttpDispatchError), + #[error("deserialization error: {0}")] + Deserialization(#[from] serde_json::Error), + #[error("{0}")] + Message(Cow<'static, str>), +} diff --git a/src/k2v-client/src/lib.rs b/src/k2v-client/src/lib.rs new file mode 100644 index 00000000..ba1cd6ea --- /dev/null +++ b/src/k2v-client/src/lib.rs @@ -0,0 +1,566 @@ +use std::collections::BTreeMap; +use std::time::Duration; + +use http::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE}; +use http::status::StatusCode; +use http::HeaderMap; + +use rusoto_core::{ByteStream, DispatchSignedRequest, HttpClient}; +use rusoto_credential::AwsCredentials; +use rusoto_signature::region::Region; +use rusoto_signature::signature::SignedRequest; +use serde::de::Error as DeError; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use tokio::io::AsyncReadExt; + +mod error; + +pub use error::Error; + +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5); +const DEFAULT_POLL_TIMEOUT: Duration = Duration::from_secs(300); +const SERVICE: &str = "k2v"; +const GARAGE_CAUSALITY_TOKEN: &str = "X-Garage-Causality-Token"; + +/// Client used to query a K2V server. +pub struct K2vClient { + region: Region, + bucket: String, + creds: AwsCredentials, + client: HttpClient, +} + +impl K2vClient { + /// Create a new K2V client. + pub fn new( + region: Region, + bucket: String, + creds: AwsCredentials, + user_agent: Option, + ) -> Result { + let mut client = HttpClient::new()?; + if let Some(ua) = user_agent { + client.local_agent_prepend(ua); + } else { + client.local_agent_prepend(format!("k2v/{}", env!("CARGO_PKG_VERSION"))); + } + Ok(K2vClient { + region, + bucket, + creds, + client, + }) + } + + /// Perform a ReadItem request, reading the value(s) stored for a single pk+sk. + pub async fn read_item( + &self, + partition_key: &str, + sort_key: &str, + ) -> Result { + let mut req = SignedRequest::new( + "GET", + SERVICE, + &self.region, + &format!("/{}/{}", self.bucket, partition_key), + ); + req.add_param("sort_key", sort_key); + req.add_header(ACCEPT, "application/octet-stream, application/json"); + + let res = self.dispatch(req, None).await?; + + let causality = res + .causality_token + .ok_or_else(|| Error::InvalidResponse("missing causality token".into()))?; + + if res.status == StatusCode::NO_CONTENT { + return Ok(CausalValue { + causality, + value: vec![K2vValue::Tombstone], + }); + } + + match res.content_type.as_deref() { + Some("application/octet-stream") => Ok(CausalValue { + causality, + value: vec![K2vValue::Value(res.body)], + }), + Some("application/json") => { + let value = serde_json::from_slice(&res.body)?; + Ok(CausalValue { causality, value }) + } + Some(ct) => Err(Error::InvalidResponse( + format!("invalid content type: {}", ct).into(), + )), + None => Err(Error::InvalidResponse("missing content type".into())), + } + } + + /// Perform a PollItem request, waiting for the value(s) stored for a single pk+sk to be + /// updated. + pub async fn poll_item( + &self, + partition_key: &str, + sort_key: &str, + causality: CausalityToken, + timeout: Option, + ) -> Result, Error> { + let timeout = timeout.unwrap_or(DEFAULT_POLL_TIMEOUT); + + let mut req = SignedRequest::new( + "GET", + SERVICE, + &self.region, + &format!("/{}/{}", self.bucket, partition_key), + ); + req.add_param("sort_key", sort_key); + req.add_param("causality_token", &causality.0); + req.add_param("timeout", &timeout.as_secs().to_string()); + req.add_header(ACCEPT, "application/octet-stream, application/json"); + + let res = self.dispatch(req, Some(timeout + DEFAULT_TIMEOUT)).await?; + + let causality = res + .causality_token + .ok_or_else(|| Error::InvalidResponse("missing causality token".into()))?; + + if res.status == StatusCode::NOT_MODIFIED { + return Ok(None); + } + + if res.status == StatusCode::NO_CONTENT { + return Ok(Some(CausalValue { + causality, + value: vec![K2vValue::Tombstone], + })); + } + + match res.content_type.as_deref() { + Some("application/octet-stream") => Ok(Some(CausalValue { + causality, + value: vec![K2vValue::Value(res.body)], + })), + Some("application/json") => { + let value = serde_json::from_slice(&res.body)?; + Ok(Some(CausalValue { causality, value })) + } + Some(ct) => Err(Error::InvalidResponse( + format!("invalid content type: {}", ct).into(), + )), + None => Err(Error::InvalidResponse("missing content type".into())), + } + } + + /// Perform an InsertItem request, inserting a value for a single pk+sk. + pub async fn insert_item( + &self, + partition_key: &str, + sort_key: &str, + value: Vec, + causality: Option, + ) -> Result<(), Error> { + let mut req = SignedRequest::new( + "PUT", + SERVICE, + &self.region, + &format!("/{}/{}", self.bucket, partition_key), + ); + req.add_param("sort_key", sort_key); + req.set_payload(Some(value)); + + if let Some(causality) = causality { + req.add_header(GARAGE_CAUSALITY_TOKEN, &causality.0); + } + + self.dispatch(req, None).await?; + Ok(()) + } + + /// Perform a DeleteItem request, deleting the value(s) stored for a single pk+sk. + pub async fn delete_item( + &self, + partition_key: &str, + sort_key: &str, + causality: CausalityToken, + ) -> Result<(), Error> { + let mut req = SignedRequest::new( + "DELETE", + SERVICE, + &self.region, + &format!("/{}/{}", self.bucket, partition_key), + ); + req.add_param("sort_key", sort_key); + req.add_header(GARAGE_CAUSALITY_TOKEN, &causality.0); + + self.dispatch(req, None).await?; + Ok(()) + } + + /// Perform a ReadIndex request, listing partition key which have at least one associated + /// sort key, and which matches the filter. + pub async fn read_index( + &self, + filter: Filter<'_>, + ) -> Result, Error> { + let mut req = + SignedRequest::new("GET", SERVICE, &self.region, &format!("/{}", self.bucket)); + filter.insert_params(&mut req); + + let res = self.dispatch(req, None).await?; + + let resp: ReadIndexResponse = serde_json::from_slice(&res.body)?; + + let items = resp + .partition_keys + .into_iter() + .map(|ReadIndexItem { pk, info }| (pk, info)) + .collect(); + + Ok(PaginatedRange { + items, + next_start: resp.next_start, + }) + } + + /// Perform an InsertBatch request, inserting multiple values at once. Note: this operation is + /// *not* atomic: it is possible for some sub-operations to fails and others to success. In + /// that case, failure is reported. + pub async fn insert_batch(&self, operations: &[BatchInsertOp<'_>]) -> Result<(), Error> { + let mut req = + SignedRequest::new("POST", SERVICE, &self.region, &format!("/{}", self.bucket)); + + let payload = serde_json::to_vec(operations)?; + req.set_payload(Some(payload)); + self.dispatch(req, None).await?; + Ok(()) + } + + /// Perform a ReadBatch request, reading multiple values or range of values at once. + pub async fn read_batch( + &self, + operations: &[BatchReadOp<'_>], + ) -> Result>, Error> { + let mut req = + SignedRequest::new("POST", SERVICE, &self.region, &format!("/{}", self.bucket)); + req.add_param("search", ""); + + let payload = serde_json::to_vec(operations)?; + req.set_payload(Some(payload)); + let res = self.dispatch(req, None).await?; + + let resp: Vec = serde_json::from_slice(&res.body)?; + + Ok(resp + .into_iter() + .map(|e| PaginatedRange { + items: e + .items + .into_iter() + .map(|BatchReadItem { sk, ct, v }| { + ( + sk, + CausalValue { + causality: ct, + value: v, + }, + ) + }) + .collect(), + next_start: e.next_start, + }) + .collect()) + } + + /// Perform a DeleteBatch request, deleting mutiple values or range of values at once, without + /// providing causality information. + pub async fn delete_batch(&self, operations: &[BatchDeleteOp<'_>]) -> Result, Error> { + let mut req = + SignedRequest::new("POST", SERVICE, &self.region, &format!("/{}", self.bucket)); + req.add_param("delete", ""); + + let payload = serde_json::to_vec(operations)?; + req.set_payload(Some(payload)); + let res = self.dispatch(req, None).await?; + + let resp: Vec = serde_json::from_slice(&res.body)?; + + Ok(resp.into_iter().map(|r| r.deleted_items).collect()) + } + + async fn dispatch( + &self, + mut req: SignedRequest, + timeout: Option, + ) -> Result { + req.sign(&self.creds); + let mut res = self + .client + .dispatch(req, Some(timeout.unwrap_or(DEFAULT_TIMEOUT))) + .await?; + + let causality_token = res + .headers + .remove(GARAGE_CAUSALITY_TOKEN) + .map(CausalityToken); + let content_type = res.headers.remove(CONTENT_TYPE); + + let body = match res.status { + StatusCode::OK => read_body(&mut res.headers, res.body).await?, + StatusCode::NO_CONTENT => Vec::new(), + StatusCode::NOT_FOUND => return Err(Error::NotFound), + StatusCode::NOT_MODIFIED => Vec::new(), + _ => { + return Err(Error::InvalidResponse( + format!("invalid error code: {}", res.status).into(), + )) + } + }; + + Ok(Response { + body, + status: res.status, + causality_token, + content_type, + }) + } +} + +async fn read_body(headers: &mut HeaderMap, body: ByteStream) -> Result, Error> { + let body_len = headers + .get(CONTENT_LENGTH) + .and_then(|h| h.parse().ok()) + .unwrap_or(0); + let mut res = Vec::with_capacity(body_len); + body.into_async_read().read_to_end(&mut res).await?; + Ok(res) +} + +/// An opaque token used to convey causality between operations. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(transparent)] +pub struct CausalityToken(String); + +impl From for CausalityToken { + fn from(v: String) -> Self { + CausalityToken(v) + } +} + +impl From for String { + fn from(v: CausalityToken) -> Self { + v.0 + } +} + +/// A value in K2V. can be either a binary value, or a tombstone. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum K2vValue { + Tombstone, + Value(Vec), +} + +impl From> for K2vValue { + fn from(v: Vec) -> Self { + K2vValue::Value(v) + } +} + +impl From>> for K2vValue { + fn from(v: Option>) -> Self { + match v { + Some(v) => K2vValue::Value(v), + None => K2vValue::Tombstone, + } + } +} + +impl<'de> Deserialize<'de> for K2vValue { + fn deserialize(d: D) -> Result + where + D: Deserializer<'de>, + { + let val: Option<&str> = Option::deserialize(d)?; + Ok(match val { + Some(s) => { + K2vValue::Value(base64::decode(s).map_err(|_| DeError::custom("invalid base64"))?) + } + None => K2vValue::Tombstone, + }) + } +} + +impl Serialize for K2vValue { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + K2vValue::Tombstone => serializer.serialize_none(), + K2vValue::Value(v) => { + let b64 = base64::encode(v); + serializer.serialize_str(&b64) + } + } + } +} + +/// A set of K2vValue and associated causality information. +#[derive(Debug, Clone, Serialize)] +pub struct CausalValue { + pub causality: CausalityToken, + pub value: Vec, +} + +/// Result of paginated requests. +#[derive(Debug, Clone)] +pub struct PaginatedRange { + pub items: BTreeMap, + pub next_start: Option, +} + +/// Filter for batch operations. +#[derive(Debug, Default, Clone, Deserialize, Serialize)] +pub struct Filter<'a> { + pub start: Option<&'a str>, + pub end: Option<&'a str>, + pub prefix: Option<&'a str>, + pub limit: Option, + #[serde(default)] + pub reverse: bool, +} + +impl<'a> Filter<'a> { + fn insert_params(&self, req: &mut SignedRequest) { + if let Some(start) = &self.start { + req.add_param("start", start); + } + if let Some(end) = &self.end { + req.add_param("end", end); + } + if let Some(prefix) = &self.prefix { + req.add_param("prefix", prefix); + } + if let Some(limit) = &self.limit { + req.add_param("limit", &limit.to_string()); + } + if self.reverse { + req.add_param("reverse", "true"); + } + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ReadIndexResponse<'a> { + #[serde(flatten, borrow)] + #[allow(dead_code)] + filter: Filter<'a>, + partition_keys: Vec, + #[allow(dead_code)] + more: bool, + next_start: Option, +} + +#[derive(Debug, Clone, Deserialize)] +struct ReadIndexItem { + pk: String, + #[serde(flatten)] + info: PartitionInfo, +} + +/// Information about data stored with a given partition key. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct PartitionInfo { + pub entries: u64, + pub conflicts: u64, + pub values: u64, + pub bytes: u64, +} + +/// Single sub-operation of an InsertBatch. +#[derive(Debug, Clone, Serialize)] +pub struct BatchInsertOp<'a> { + #[serde(rename = "pk")] + pub partition_key: &'a str, + #[serde(rename = "sk")] + pub sort_key: &'a str, + #[serde(rename = "ct")] + pub causality: Option, + #[serde(rename = "v")] + pub value: K2vValue, +} + +/// Single sub-operation of a ReadBatch. +#[derive(Debug, Default, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BatchReadOp<'a> { + pub partition_key: &'a str, + #[serde(flatten, borrow)] + pub filter: Filter<'a>, + #[serde(default)] + pub single_item: bool, + #[serde(default)] + pub conflicts_only: bool, + #[serde(default)] + pub tombstones: bool, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +struct BatchReadResponse<'a> { + #[serde(flatten, borrow)] + #[allow(dead_code)] + op: BatchReadOp<'a>, + items: Vec, + #[allow(dead_code)] + more: bool, + next_start: Option, +} + +#[derive(Debug, Clone, Deserialize)] +struct BatchReadItem { + sk: String, + ct: CausalityToken, + v: Vec, +} + +/// Single sub-operation of a DeleteBatch +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BatchDeleteOp<'a> { + pub partition_key: &'a str, + pub prefix: Option<&'a str>, + pub start: Option<&'a str>, + pub end: Option<&'a str>, + #[serde(default)] + pub single_item: bool, +} + +impl<'a> BatchDeleteOp<'a> { + pub fn new(partition_key: &'a str) -> Self { + BatchDeleteOp { + partition_key, + prefix: None, + start: None, + end: None, + single_item: false, + } + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +struct BatchDeleteResponse<'a> { + #[serde(flatten, borrow)] + #[allow(dead_code)] + filter: BatchDeleteOp<'a>, + deleted_items: u64, +} + +struct Response { + body: Vec, + status: StatusCode, + causality_token: Option, + content_type: Option, +} diff --git a/src/util/formater.rs b/src/util/formater.rs new file mode 100644 index 00000000..95324f9a --- /dev/null +++ b/src/util/formater.rs @@ -0,0 +1,28 @@ +pub fn format_table(data: Vec) { + let data = data + .iter() + .map(|s| s.split('\t').collect::>()) + .collect::>(); + + let columns = data.iter().map(|row| row.len()).fold(0, std::cmp::max); + let mut column_size = vec![0; columns]; + + let mut out = String::new(); + + for row in data.iter() { + for (i, col) in row.iter().enumerate() { + column_size[i] = std::cmp::max(column_size[i], col.chars().count()); + } + } + + for row in data.iter() { + for (col, col_len) in row[..row.len() - 1].iter().zip(column_size.iter()) { + out.push_str(col); + (0..col_len - col.chars().count() + 2).for_each(|_| out.push(' ')); + } + out.push_str(row[row.len() - 1]); + out.push('\n'); + } + + print!("{}", out); +} diff --git a/src/util/lib.rs b/src/util/lib.rs index e83fc2e6..d8ffdd0b 100644 --- a/src/util/lib.rs +++ b/src/util/lib.rs @@ -8,6 +8,7 @@ pub mod config; pub mod crdt; pub mod data; pub mod error; +pub mod formater; pub mod metrics; pub mod persister; pub mod sled_counter; From 382e74c798263d042b1c6ca3788c866a8c69c4f4 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 24 May 2022 12:16:39 +0200 Subject: [PATCH 008/149] First version of admin API (#298) **Spec:** - [x] Start writing - [x] Specify all layout endpoints - [x] Specify all endpoints for operations on keys - [x] Specify all endpoints for operations on key/bucket permissions - [x] Specify all endpoints for operations on buckets - [x] Specify all endpoints for operations on bucket aliases View rendered spec at **Code:** - [x] Refactor code for admin api to use common api code that was created for K2V **General endpoints:** - [x] Metrics - [x] GetClusterStatus - [x] ConnectClusterNodes - [x] GetClusterLayout - [x] UpdateClusterLayout - [x] ApplyClusterLayout - [x] RevertClusterLayout **Key-related endpoints:** - [x] ListKeys - [x] CreateKey - [x] ImportKey - [x] GetKeyInfo - [x] UpdateKey - [x] DeleteKey **Bucket-related endpoints:** - [x] ListBuckets - [x] CreateBucket - [x] GetBucketInfo - [x] DeleteBucket - [x] PutBucketWebsite - [x] DeleteBucketWebsite **Operations on key/bucket permissions:** - [x] BucketAllowKey - [x] BucketDenyKey **Operations on bucket aliases:** - [x] GlobalAliasBucket - [x] GlobalUnaliasBucket - [x] LocalAliasBucket - [x] LocalUnaliasBucket **And also:** - [x] Separate error type for the admin API (this PR includes a quite big refactoring of error handling) - [x] Add management of website access - [ ] Check that nothing is missing wrt what can be done using the CLI - [ ] Improve formatting of the spec - [x] Make sure everyone is cool with the API design Fix #231 Fix #295 Co-authored-by: Alex Auvolat Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/298 Co-authored-by: Alex Co-committed-by: Alex --- Cargo.lock | 28 +- Cargo.nix | 193 ++++---- Cargo.toml | 1 - Makefile | 2 +- doc/drafts/admin-api.md | 603 +++++++++++++++++++++++++ src/admin/Cargo.toml | 29 -- src/admin/lib.rs | 6 - src/admin/metrics.rs | 146 ------ src/api/Cargo.toml | 3 + src/api/admin/api_server.rs | 199 ++++++++ src/api/admin/bucket.rs | 549 ++++++++++++++++++++++ src/api/admin/cluster.rs | 198 ++++++++ src/api/admin/error.rs | 96 ++++ src/api/admin/key.rs | 264 +++++++++++ src/api/admin/mod.rs | 7 + src/api/admin/router.rs | 149 ++++++ src/api/common_error.rs | 177 ++++++++ src/api/generic_server.rs | 21 +- src/api/helpers.rs | 53 +-- src/api/k2v/api_server.rs | 33 +- src/api/k2v/batch.rs | 19 +- src/api/k2v/error.rs | 134 ++++++ src/api/k2v/index.rs | 2 +- src/api/k2v/item.rs | 2 +- src/api/k2v/mod.rs | 1 + src/api/k2v/range.rs | 4 +- src/api/k2v/router.rs | 6 +- src/api/lib.rs | 6 +- src/api/router_macros.rs | 33 +- src/api/s3/api_server.rs | 28 +- src/api/s3/bucket.rs | 24 +- src/api/s3/copy.rs | 25 +- src/api/s3/cors.rs | 34 +- src/api/s3/delete.rs | 2 +- src/api/{ => s3}/error.rs | 247 ++++------ src/api/s3/get.rs | 10 +- src/api/s3/list.rs | 14 +- src/api/s3/mod.rs | 1 + src/api/s3/post_object.rs | 62 ++- src/api/s3/put.rs | 18 +- src/api/s3/router.rs | 5 +- src/api/s3/website.rs | 49 +- src/api/s3/xml.rs | 2 +- src/api/signature/error.rs | 36 ++ src/api/signature/mod.rs | 7 +- src/api/signature/payload.rs | 20 +- src/api/signature/streaming.rs | 8 +- src/garage/Cargo.toml | 7 +- src/garage/admin.rs | 77 +--- src/garage/cli/layout.rs | 47 +- src/garage/main.rs | 2 + src/garage/server.rs | 36 +- src/{admin => garage}/tracing_setup.rs | 0 src/model/garage.rs | 4 + src/model/helper/bucket.rs | 152 ++++--- src/model/helper/error.rs | 10 + src/model/helper/key.rs | 102 +++++ src/model/helper/mod.rs | 1 + src/rpc/Cargo.toml | 2 +- src/rpc/layout.rs | 56 +++ src/rpc/system.rs | 132 ++++-- src/util/config.rs | 4 + src/util/crdt/lww_map.rs | 5 + src/web/error.rs | 36 +- src/web/web_server.rs | 10 +- 65 files changed, 3253 insertions(+), 986 deletions(-) create mode 100644 doc/drafts/admin-api.md delete mode 100644 src/admin/Cargo.toml delete mode 100644 src/admin/lib.rs delete mode 100644 src/admin/metrics.rs create mode 100644 src/api/admin/api_server.rs create mode 100644 src/api/admin/bucket.rs create mode 100644 src/api/admin/cluster.rs create mode 100644 src/api/admin/error.rs create mode 100644 src/api/admin/key.rs create mode 100644 src/api/admin/mod.rs create mode 100644 src/api/admin/router.rs create mode 100644 src/api/common_error.rs create mode 100644 src/api/k2v/error.rs rename src/api/{ => s3}/error.rs (50%) create mode 100644 src/api/signature/error.rs rename src/{admin => garage}/tracing_setup.rs (100%) create mode 100644 src/model/helper/key.rs diff --git a/Cargo.lock b/Cargo.lock index a4476273..fcf3030a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -888,21 +888,23 @@ dependencies = [ "chrono", "futures", "futures-util", - "garage_admin", "garage_api", "garage_model 0.7.0", "garage_rpc 0.7.0", "garage_table 0.7.0", "garage_util 0.7.0", "garage_web", - "git-version", "hex", "hmac 0.10.1", "http", "hyper", "kuska-sodiumoxide", "netapp 0.4.4", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry-prometheus", "pretty_env_logger", + "prometheus", "rand 0.8.5", "rmp-serde 0.15.5", "serde", @@ -917,23 +919,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "garage_admin" -version = "0.7.0" -dependencies = [ - "futures", - "futures-util", - "garage_util 0.7.0", - "hex", - "http", - "hyper", - "opentelemetry", - "opentelemetry-otlp", - "opentelemetry-prometheus", - "prometheus", - "tracing", -] - [[package]] name = "garage_api" version = "0.7.0" @@ -963,8 +948,11 @@ dependencies = [ "multer", "nom", "opentelemetry", + "opentelemetry-otlp", + "opentelemetry-prometheus", "percent-encoding", "pin-project 1.0.10", + "prometheus", "quick-xml", "roxmltree", "serde", @@ -1089,9 +1077,9 @@ dependencies = [ "bytes 1.1.0", "futures", "futures-util", - "garage_admin", "garage_util 0.7.0", "gethostname", + "git-version", "hex", "hyper", "k8s-openapi", diff --git a/Cargo.nix b/Cargo.nix index 9c936ed0..371ce8d3 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -6,7 +6,6 @@ args@{ rootFeatures ? [ "garage_util/default" "garage_rpc/default" - "garage_admin/default" "garage_table/default" "garage_block/default" "garage_model/default" @@ -48,7 +47,6 @@ in workspace = { garage_util = rustPackages.unknown.garage_util."0.7.0"; garage_rpc = rustPackages.unknown.garage_rpc."0.7.0"; - garage_admin = rustPackages.unknown.garage_admin."0.7.0"; garage_table = rustPackages.unknown.garage_table."0.7.0"; garage_block = rustPackages.unknown.garage_block."0.7.0"; garage_model = rustPackages.unknown.garage_model."0.7.0"; @@ -1220,7 +1218,7 @@ in [ "async-await" ] [ "async-await-macro" ] [ "channel" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "default") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "default") [ "futures-channel" ] [ "futures-io" ] [ "futures-macro" ] @@ -1269,18 +1267,20 @@ in bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; - garage_admin = rustPackages."unknown".garage_admin."0.7.0" { inherit profileName; }; garage_api = rustPackages."unknown".garage_api."0.7.0" { inherit profileName; }; garage_model = rustPackages."unknown".garage_model."0.7.0" { inherit profileName; }; garage_rpc = rustPackages."unknown".garage_rpc."0.7.0" { inherit profileName; }; garage_table = rustPackages."unknown".garage_table."0.7.0" { inherit profileName; }; garage_util = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; garage_web = rustPackages."unknown".garage_web."0.7.0" { inherit profileName; }; - git_version = rustPackages."registry+https://github.com/rust-lang/crates.io-index".git-version."0.3.5" { inherit profileName; }; hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; sodiumoxide = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; + opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; + opentelemetry_otlp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-otlp."0.10.0" { inherit profileName; }; + opentelemetry_prometheus = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-prometheus."0.10.0" { inherit profileName; }; pretty_env_logger = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pretty_env_logger."0.4.0" { inherit profileName; }; + prometheus = rustPackages."registry+https://github.com/rust-lang/crates.io-index".prometheus."0.13.0" { inherit profileName; }; rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; @@ -1305,26 +1305,6 @@ in }; }); - "unknown".garage_admin."0.7.0" = overridableMkRustCrate (profileName: rec { - name = "garage_admin"; - version = "0.7.0"; - registry = "unknown"; - src = fetchCrateLocal (workspaceSrc + "/src/admin"); - dependencies = { - futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; - futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; - garage_util = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; - hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; - http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; - hyper = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; - opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; - opentelemetry_otlp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-otlp."0.10.0" { inherit profileName; }; - opentelemetry_prometheus = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-prometheus."0.10.0" { inherit profileName; }; - prometheus = rustPackages."registry+https://github.com/rust-lang/crates.io-index".prometheus."0.13.0" { inherit profileName; }; - tracing = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; - }; - }); - "unknown".garage_api."0.7.0" = overridableMkRustCrate (profileName: rec { name = "garage_api"; version = "0.7.0"; @@ -1359,8 +1339,11 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "multer" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".multer."2.0.2" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "nom" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".nom."7.1.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "opentelemetry_otlp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-otlp."0.10.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "opentelemetry_prometheus" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-prometheus."0.10.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "percent_encoding" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".percent-encoding."2.1.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "pin_project" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "prometheus" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".prometheus."0.13.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "quick_xml" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quick-xml."0.21.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "roxmltree" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".roxmltree."0.14.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; @@ -1506,9 +1489,9 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "garage_admin" else null } = rustPackages."unknown".garage_admin."0.7.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "gethostname" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".gethostname."0.2.3" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "git_version" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".git-version."0.3.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "hyper" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "k8s_openapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".k8s-openapi."0.13.1" { inherit profileName; }; @@ -1898,31 +1881,31 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2"; }; features = builtins.concatLists [ - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "client") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "client") [ "default" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "full") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "h2") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "http1") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "http2") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "runtime") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "server") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "socket2") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "stream") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "tcp") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "full") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "h2") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "http1") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "http2") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "runtime") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "server") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "socket2") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "stream") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "tcp") ]; dependencies = { bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; futures_channel = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-channel."0.3.21" { inherit profileName; }; futures_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "h2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".h2."0.3.12" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "h2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".h2."0.3.12" { inherit profileName; }; http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; http_body = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."0.4.4" { inherit profileName; }; httparse = rustPackages."registry+https://github.com/rust-lang/crates.io-index".httparse."1.6.0" { inherit profileName; }; httpdate = rustPackages."registry+https://github.com/rust-lang/crates.io-index".httpdate."1.0.2" { inherit profileName; }; itoa = rustPackages."registry+https://github.com/rust-lang/crates.io-index".itoa."1.0.1" { inherit profileName; }; pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "socket2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".socket2."0.4.4" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "socket2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".socket2."0.4.4" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; tower_service = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.1" { inherit profileName; }; tracing = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; @@ -3341,7 +3324,7 @@ in [ "getrandom" ] [ "libc" ] [ "rand_chacha" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "small_rng") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "small_rng") [ "std" ] [ "std_rng" ] ]; @@ -3434,28 +3417,28 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"; }; features = builtins.concatLists [ - [ "aho-corasick" ] - [ "default" ] - [ "memchr" ] - [ "perf" ] - [ "perf-cache" ] - [ "perf-dfa" ] - [ "perf-inline" ] - [ "perf-literal" ] - [ "std" ] - [ "unicode" ] - [ "unicode-age" ] - [ "unicode-bool" ] - [ "unicode-case" ] - [ "unicode-gencat" ] - [ "unicode-perl" ] - [ "unicode-script" ] - [ "unicode-segment" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "aho-corasick") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "default") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "memchr") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "perf") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "perf-cache") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "perf-dfa") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "perf-inline") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "perf-literal") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "std") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode-age") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode-bool") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode-case") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode-gencat") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode-perl") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode-script") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode-segment") ]; dependencies = { - aho_corasick = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aho-corasick."0.7.18" { inherit profileName; }; - memchr = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; }; - regex_syntax = rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex-syntax."0.6.25" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "aho_corasick" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aho-corasick."0.7.18" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "memchr" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "regex_syntax" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex-syntax."0.6.25" { inherit profileName; }; }; }); @@ -4302,8 +4285,8 @@ in [ "bytes" ] [ "default" ] [ "fs" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "full") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "io-std") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "full") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "io-std") [ "io-util" ] [ "libc" ] [ "macros" ] @@ -4312,8 +4295,8 @@ in [ "net" ] [ "num_cpus" ] [ "once_cell" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "parking_lot") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "process") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "parking_lot") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "process") [ "rt" ] [ "rt-multi-thread" ] [ "signal" ] @@ -4331,7 +4314,7 @@ in mio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".mio."0.8.2" { inherit profileName; }; num_cpus = rustPackages."registry+https://github.com/rust-lang/crates.io-index".num_cpus."1.13.1" { inherit profileName; }; once_cell = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.12.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.12.0" { inherit profileName; }; pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; ${ if hostPlatform.isUnix then "signal_hook_registry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".signal-hook-registry."1.4.0" { inherit profileName; }; socket2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".socket2."0.4.4" { inherit profileName; }; @@ -4409,9 +4392,9 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0"; }; features = builtins.concatLists [ - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "codec") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "codec") [ "compat" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") [ "futures-io" ] (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "io") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "slab") @@ -4528,43 +4511,43 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "9a89fd63ad6adf737582df5db40d286574513c69a11dac5214dc3b5603d6713e"; }; features = builtins.concatLists [ - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "__common") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "balance") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "buffer") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "default") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "discover") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "futures-core") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "futures-util") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "indexmap") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "limit") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "load") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "log") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "make") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "pin-project") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "pin-project-lite") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "rand") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "ready-cache") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "__common") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "balance") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "buffer") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "default") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "discover") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "futures-core") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "futures-util") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "indexmap") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "limit") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "load") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "log") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "make") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "pin-project") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "pin-project-lite") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "rand") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "ready-cache") (lib.optional (rootFeatures' ? "garage") "retry") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "slab") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "timeout") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "tokio") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "tokio-util") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "tracing") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "util") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "slab") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "timeout") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "tokio") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "tokio-util") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "tracing") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "util") ]; dependencies = { - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "futures_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "indexmap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".indexmap."1.8.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "pin_project" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "pin_project_lite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "slab" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".slab."0.4.5" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "tokio_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "tower_layer" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-layer."0.3.1" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "tower_service" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.1" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "tracing" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "futures_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "indexmap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".indexmap."1.8.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "pin_project" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "pin_project_lite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "slab" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".slab."0.4.5" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "tokio_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "tower_layer" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-layer."0.3.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "tower_service" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "tracing" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; }; }); @@ -4617,14 +4600,14 @@ in features = builtins.concatLists [ [ "attributes" ] [ "default" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "log") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "log") (lib.optional (rootFeatures' ? "garage") "log-always") [ "std" ] [ "tracing-attributes" ] ]; dependencies = { cfg_if = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "log" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "log" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; tracing_attributes = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-attributes."0.1.20" { profileName = "__noProfile"; }; tracing_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-core."0.1.23" { inherit profileName; }; @@ -4986,7 +4969,7 @@ in [ "std" ] [ "synchapi" ] [ "sysinfoapi" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "threadpoollegacyapiset") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "threadpoollegacyapiset") [ "timezoneapi" ] [ "winbase" ] [ "wincon" ] @@ -5049,8 +5032,8 @@ in ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); diff --git a/Cargo.toml b/Cargo.toml index c64f7897..edd0e3f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,6 @@ members = [ "src/table", "src/block", "src/model", - "src/admin", "src/api", "src/web", "src/garage", diff --git a/Makefile b/Makefile index c70be9da..eeeffedb 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: doc all release shell all: - clear; cargo build --features k2v + clear; cargo build --all-features doc: cd doc/book; mdbook build diff --git a/doc/drafts/admin-api.md b/doc/drafts/admin-api.md new file mode 100644 index 00000000..b35a87f1 --- /dev/null +++ b/doc/drafts/admin-api.md @@ -0,0 +1,603 @@ +# Specification of Garage's administration API + + +**WARNING.** At this point, there is no comittement to stability of the APIs described in this document. +We will bump the version numbers prefixed to each API endpoint at each time the syntax +or semantics change, meaning that code that relies on these endpoint will break +when changes are introduced. + + +## Access control + +The admin API uses two different tokens for acces control, that are specified in the config file's `[admin]` section: + +- `metrics_token`: the token for accessing the Metrics endpoint (if this token is not set in the config file, the Metrics endpoint can be accessed without access control); +- `admin_token`: the token for accessing all of the other administration endpoints (if this token is not set in the config file, access to these endpoints is disabled entirely). + +## Administration API endpoints + +### Metrics-related endpoints + +#### Metrics `GET /metrics` + +Returns internal Garage metrics in Prometheus format. + +### Cluster operations + +#### GetClusterStatus `GET /v0/status` + +Returns the cluster's current status in JSON, including: + +- ID of the node being queried and its version of the Garage daemon +- Live nodes +- Currently configured cluster layout +- Staged changes to the cluster layout + +Example response body: + +```json +{ + "node": "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f", + "garage_version": "git:v0.8.0", + "knownNodes": { + "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f": { + "addr": "10.0.0.11:3901", + "is_up": true, + "last_seen_secs_ago": 9, + "hostname": "node1" + }, + "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff": { + "addr": "10.0.0.12:3901", + "is_up": true, + "last_seen_secs_ago": 1, + "hostname": "node2" + }, + "23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27": { + "addr": "10.0.0.21:3901", + "is_up": true, + "last_seen_secs_ago": 7, + "hostname": "node3" + }, + "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b": { + "addr": "10.0.0.22:3901", + "is_up": true, + "last_seen_secs_ago": 1, + "hostname": "node4" + } + }, + "layout": { + "version": 12, + "roles": { + "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f": { + "zone": "dc1", + "capacity": 4, + "tags": [ + "node1" + ] + }, + "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff": { + "zone": "dc1", + "capacity": 6, + "tags": [ + "node2" + ] + }, + "23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27": { + "zone": "dc2", + "capacity": 10, + "tags": [ + "node3" + ] + } + }, + "stagedRoleChanges": { + "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b": { + "zone": "dc2", + "capacity": 5, + "tags": [ + "node4" + ] + } + } + } +} +``` + +#### ConnectClusterNodes `POST /v0/connect` + +Instructs this Garage node to connect to other Garage nodes at specified addresses. + +Example request body: + +```json +[ + "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f@10.0.0.11:3901", + "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff@10.0.0.12:3901" +] +``` + +The format of the string for a node to connect to is: `@:`, same as in the `garage node connect` CLI call. + +Example response: + +```json +[ + { + "success": true, + "error": null + }, + { + "success": false, + "error": "Handshake error" + } +] +``` + +#### GetClusterLayout `GET /v0/layout` + +Returns the cluster's current layout in JSON, including: + +- Currently configured cluster layout +- Staged changes to the cluster layout + +(the info returned by this endpoint is a subset of the info returned by GetClusterStatus) + +Example response body: + +```json +{ + "version": 12, + "roles": { + "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f": { + "zone": "dc1", + "capacity": 4, + "tags": [ + "node1" + ] + }, + "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff": { + "zone": "dc1", + "capacity": 6, + "tags": [ + "node2" + ] + }, + "23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27": { + "zone": "dc2", + "capacity": 10, + "tags": [ + "node3" + ] + } + }, + "stagedRoleChanges": { + "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b": { + "zone": "dc2", + "capacity": 5, + "tags": [ + "node4" + ] + } + } +} +``` + +#### UpdateClusterLayout `POST /v0/layout` + +Send modifications to the cluster layout. These modifications will +be included in the staged role changes, visible in subsequent calls +of `GetClusterLayout`. Once the set of staged changes is satisfactory, +the user may call `ApplyClusterLayout` to apply the changed changes, +or `Revert ClusterLayout` to clear all of the staged changes in +the layout. + +Request body format: + +```json +{ + : { + "capacity": , + "zone": , + "tags": [ + , + ... + ] + }, + : null, + ... +} +``` + +Contrary to the CLI that may update only a subset of the fields +`capacity`, `zone` and `tags`, when calling this API all of these +values must be specified. + + +#### ApplyClusterLayout `POST /v0/layout/apply` + +Applies to the cluster the layout changes currently registered as +staged layout changes. + +Request body format: + +```json +{ + "version": 13 +} +``` + +Similarly to the CLI, the body must include the version of the new layout +that will be created, which MUST be 1 + the value of the currently +existing layout in the cluster. + +#### RevertClusterLayout `POST /v0/layout/revert` + +Clears all of the staged layout changes. + +Request body format: + +```json +{ + "version": 13 +} +``` + +Reverting the staged changes is done by incrementing the version number +and clearing the contents of the staged change list. +Similarly to the CLI, the body must include the incremented +version number, which MUST be 1 + the value of the currently +existing layout in the cluster. + + +### Access key operations + +#### ListKeys `GET /v0/key` + +Returns all API access keys in the cluster. + +Example response: + +```json +[ + { + "id": "GK31c2f218a2e44f485b94239e", + "name": "test" + }, + { + "id": "GKe10061ac9c2921f09e4c5540", + "name": "test2" + } +] +``` + +#### CreateKey `POST /v0/key` + +Creates a new API access key. + +Request body format: + +```json +{ + "name": "NameOfMyKey" +} +``` + +#### ImportKey `POST /v0/key/import` + +Imports an existing API key. + +Request body format: + +```json +{ + "accessKeyId": "GK31c2f218a2e44f485b94239e", + "secretAccessKey": "b892c0665f0ada8a4755dae98baa3b133590e11dae3bcc1f9d769d67f16c3835", + "name": "NameOfMyKey" +} +``` + +#### GetKeyInfo `GET /v0/key?id=` +#### GetKeyInfo `GET /v0/key?search=` + +Returns information about the requested API access key. + +If `id` is set, the key is looked up using its exact identifier (faster). +If `search` is set, the key is looked up using its name or prefix +of identifier (slower, all keys are enumerated to do this). + +Example response: + +```json +{ + "name": "test", + "accessKeyId": "GK31c2f218a2e44f485b94239e", + "secretAccessKey": "b892c0665f0ada8a4755dae98baa3b133590e11dae3bcc1f9d769d67f16c3835", + "permissions": { + "createBucket": false + }, + "buckets": [ + { + "id": "70dc3bed7fe83a75e46b66e7ddef7d56e65f3c02f9f80b6749fb97eccb5e1033", + "globalAliases": [ + "test2" + ], + "localAliases": [], + "permissions": { + "read": true, + "write": true, + "owner": false + } + }, + { + "id": "d7452a935e663fc1914f3a5515163a6d3724010ce8dfd9e4743ca8be5974f995", + "globalAliases": [ + "test3" + ], + "localAliases": [], + "permissions": { + "read": true, + "write": true, + "owner": false + } + }, + { + "id": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b", + "globalAliases": [], + "localAliases": [ + "test" + ], + "permissions": { + "read": true, + "write": true, + "owner": true + } + }, + { + "id": "96470e0df00ec28807138daf01915cfda2bee8eccc91dea9558c0b4855b5bf95", + "globalAliases": [ + "alex" + ], + "localAliases": [], + "permissions": { + "read": true, + "write": true, + "owner": true + } + } + ] +} +``` + +#### DeleteKey `DELETE /v0/key?id=` + +Deletes an API access key. + +#### UpdateKey `POST /v0/key?id=` + +Updates information about the specified API access key. + +Request body format: + +```json +{ + "name": "NameOfMyKey", + "allow": { + "createBucket": true, + }, + "deny": {} +} +``` + +All fields (`name`, `allow` and `deny`) are optionnal. +If they are present, the corresponding modifications are applied to the key, otherwise nothing is changed. +The possible flags in `allow` and `deny` are: `createBucket`. + + +### Bucket operations + +#### ListBuckets `GET /v0/bucket` + +Returns all storage buckets in the cluster. + +Example response: + +```json +[ + { + "id": "70dc3bed7fe83a75e46b66e7ddef7d56e65f3c02f9f80b6749fb97eccb5e1033", + "globalAliases": [ + "test2" + ], + "localAliases": [] + }, + { + "id": "96470e0df00ec28807138daf01915cfda2bee8eccc91dea9558c0b4855b5bf95", + "globalAliases": [ + "alex" + ], + "localAliases": [] + }, + { + "id": "d7452a935e663fc1914f3a5515163a6d3724010ce8dfd9e4743ca8be5974f995", + "globalAliases": [ + "test3" + ], + "localAliases": [] + }, + { + "id": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b", + "globalAliases": [], + "localAliases": [ + { + "accessKeyId": "GK31c2f218a2e44f485b94239e", + "alias": "test" + } + ] + } +] +``` + +#### GetBucketInfo `GET /v0/bucket?id=` +#### GetBucketInfo `GET /v0/bucket?globalAlias=` + +Returns information about the requested storage bucket. + +If `id` is set, the bucket is looked up using its exact identifier. +If `globalAlias` is set, the bucket is looked up using its global alias. +(both are fast) + +Example response: + +```json +{ + "id": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b", + "globalAliases": [ + "alex" + ], + "keys": [ + { + "accessKeyId": "GK31c2f218a2e44f485b94239e", + "name": "alex", + "permissions": { + "read": true, + "write": true, + "owner": true + }, + "bucketLocalAliases": [ + "test" + ] + } + ] +} +``` + +#### CreateBucket `POST /v0/bucket` + +Creates a new storage bucket. + +Request body format: + +```json +{ + "globalAlias": "NameOfMyBucket" +} +``` + +OR + +```json +{ + "localAlias": { + "accessKeyId": "GK31c2f218a2e44f485b94239e", + "alias": "NameOfMyBucket", + "allow": { + "read": true, + "write": true, + "owner": false + } + } +} +``` + +OR + +```json +{} +``` + +Creates a new bucket, either with a global alias, a local one, +or no alias at all. + +Technically, you can also specify both `globalAlias` and `localAlias` and that would create +two aliases, but I don't see why you would want to do that. + +#### DeleteBucket `DELETE /v0/bucket?id=` + +Deletes a storage bucket. A bucket cannot be deleted if it is not empty. + +Warning: this will delete all aliases associated with the bucket! + +#### PutBucketWebsite `PUT /v0/bucket/website?id=` + +Sets the website configuration for a bucket (this also enables website access for this bucket). + +Request body format: + +```json +{ + "indexDocument": "index.html", + "errorDocument": "404.html" +} +``` + +The field `errorDocument` is optional, if no error document is set a generic error message is displayed when errors happen. + + +#### DeleteBucketWebsite `DELETE /v0/bucket/website?id=` + +Deletes the website configuration for a bucket (disables website access for this bucket). + + +### Operations on permissions for keys on buckets + +#### BucketAllowKey `POST /v0/bucket/allow` + +Allows a key to do read/write/owner operations on a bucket. + +Request body format: + +```json +{ + "bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b", + "accessKeyId": "GK31c2f218a2e44f485b94239e", + "permissions": { + "read": true, + "write": true, + "owner": true + }, +} +``` + +Flags in `permissions` which have the value `true` will be activated. +Other flags will remain unchanged. + +#### BucketDenyKey `POST /v0/bucket/deny` + +Denies a key from doing read/write/owner operations on a bucket. + +Request body format: + +```json +{ + "bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b", + "accessKeyId": "GK31c2f218a2e44f485b94239e", + "permissions": { + "read": false, + "write": false, + "owner": true + }, +} +``` + +Flags in `permissions` which have the value `true` will be deactivated. +Other flags will remain unchanged. + + +### Operations on bucket aliases + +#### GlobalAliasBucket `PUT /v0/bucket/alias/global?id=&alias=` + +Empty body. Creates a global alias for a bucket. + +#### GlobalUnaliasBucket `DELETE /v0/bucket/alias/global?id=&alias=` + +Removes a global alias for a bucket. + +#### LocalAliasBucket `PUT /v0/bucket/alias/local?id=&accessKeyId=&alias=` + +Empty body. Creates a local alias for a bucket in the namespace of a specific access key. + +#### LocalUnaliasBucket `DELETE /v0/bucket/alias/local?id=&accessKeyId&alias=` + +Removes a local alias for a bucket in the namespace of a specific access key. + diff --git a/src/admin/Cargo.toml b/src/admin/Cargo.toml deleted file mode 100644 index 2db4bb08..00000000 --- a/src/admin/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "garage_admin" -version = "0.7.0" -authors = ["Maximilien Richer "] -edition = "2018" -license = "AGPL-3.0" -description = "Administration and metrics REST HTTP server for Garage" -repository = "https://git.deuxfleurs.fr/Deuxfleurs/garage" - -[lib] -path = "lib.rs" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -garage_util = { version = "0.7.0", path = "../util" } - -hex = "0.4" - -futures = "0.3" -futures-util = "0.3" -http = "0.2" -hyper = "0.14" -tracing = "0.1.30" - -opentelemetry = { version = "0.17", features = [ "rt-tokio" ] } -opentelemetry-prometheus = "0.10" -opentelemetry-otlp = "0.10" -prometheus = "0.13" diff --git a/src/admin/lib.rs b/src/admin/lib.rs deleted file mode 100644 index b5b0775b..00000000 --- a/src/admin/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Crate for handling the admin and metric HTTP APIs -#[macro_use] -extern crate tracing; - -pub mod metrics; -pub mod tracing_setup; diff --git a/src/admin/metrics.rs b/src/admin/metrics.rs deleted file mode 100644 index 7edc36c6..00000000 --- a/src/admin/metrics.rs +++ /dev/null @@ -1,146 +0,0 @@ -use std::convert::Infallible; -use std::net::SocketAddr; -use std::sync::Arc; -use std::time::SystemTime; - -use futures::future::*; -use hyper::{ - header::CONTENT_TYPE, - service::{make_service_fn, service_fn}, - Body, Method, Request, Response, Server, -}; - -use opentelemetry::{ - global, - metrics::{BoundCounter, BoundValueRecorder}, - trace::{FutureExt, TraceContextExt, Tracer}, - Context, -}; -use opentelemetry_prometheus::PrometheusExporter; - -use prometheus::{Encoder, TextEncoder}; - -use garage_util::error::Error as GarageError; -use garage_util::metrics::*; - -// serve_req on metric endpoint -async fn serve_req( - req: Request, - admin_server: Arc, -) -> Result, hyper::Error> { - debug!("Receiving request at path {}", req.uri()); - let request_start = SystemTime::now(); - - admin_server.metrics.http_counter.add(1); - - let response = match (req.method(), req.uri().path()) { - (&Method::GET, "/metrics") => { - let mut buffer = vec![]; - let encoder = TextEncoder::new(); - - let tracer = opentelemetry::global::tracer("garage"); - let metric_families = tracer.in_span("admin/gather_metrics", |_| { - admin_server.exporter.registry().gather() - }); - - encoder.encode(&metric_families, &mut buffer).unwrap(); - admin_server - .metrics - .http_body_gauge - .record(buffer.len() as u64); - - Response::builder() - .status(200) - .header(CONTENT_TYPE, encoder.format_type()) - .body(Body::from(buffer)) - .unwrap() - } - _ => Response::builder() - .status(404) - .body(Body::from("Not implemented")) - .unwrap(), - }; - - admin_server - .metrics - .http_req_histogram - .record(request_start.elapsed().map_or(0.0, |d| d.as_secs_f64())); - Ok(response) -} - -// AdminServer hold the admin server internal admin_server and the metric exporter -pub struct AdminServer { - exporter: PrometheusExporter, - metrics: AdminServerMetrics, -} - -// GarageMetricadmin_server holds the metrics counter definition for Garage -// FIXME: we would rather have that split up among the different libraries? -struct AdminServerMetrics { - http_counter: BoundCounter, - http_body_gauge: BoundValueRecorder, - http_req_histogram: BoundValueRecorder, -} - -impl AdminServer { - /// init initilialize the AdminServer and background metric server - pub fn init() -> AdminServer { - let exporter = opentelemetry_prometheus::exporter().init(); - let meter = global::meter("garage/admin_server"); - AdminServer { - exporter, - metrics: AdminServerMetrics { - http_counter: meter - .u64_counter("admin.http_requests_total") - .with_description("Total number of HTTP requests made.") - .init() - .bind(&[]), - http_body_gauge: meter - .u64_value_recorder("admin.http_response_size_bytes") - .with_description("The metrics HTTP response sizes in bytes.") - .init() - .bind(&[]), - http_req_histogram: meter - .f64_value_recorder("admin.http_request_duration_seconds") - .with_description("The HTTP request latencies in seconds.") - .init() - .bind(&[]), - }, - } - } - /// run execute the admin server on the designated HTTP port and listen for requests - pub async fn run( - self, - bind_addr: SocketAddr, - shutdown_signal: impl Future, - ) -> Result<(), GarageError> { - let admin_server = Arc::new(self); - // For every connection, we must make a `Service` to handle all - // incoming HTTP requests on said connection. - let make_svc = make_service_fn(move |_conn| { - let admin_server = admin_server.clone(); - // This is the `Service` that will handle the connection. - // `service_fn` is a helper to convert a function that - // returns a Response into a `Service`. - async move { - Ok::<_, Infallible>(service_fn(move |req| { - let tracer = opentelemetry::global::tracer("garage"); - let span = tracer - .span_builder("admin/request") - .with_trace_id(gen_trace_id()) - .start(&tracer); - - serve_req(req, admin_server.clone()) - .with_context(Context::current_with_span(span)) - })) - } - }); - - let server = Server::bind(&bind_addr).serve(make_svc); - let graceful = server.with_graceful_shutdown(shutdown_signal); - info!("Admin server listening on http://{}", bind_addr); - - graceful.await?; - Ok(()) - } -} diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml index 29b26e5e..db77cf38 100644 --- a/src/api/Cargo.toml +++ b/src/api/Cargo.toml @@ -54,6 +54,9 @@ quick-xml = { version = "0.21", features = [ "serialize" ] } url = "2.1" opentelemetry = "0.17" +opentelemetry-prometheus = "0.10" +opentelemetry-otlp = "0.10" +prometheus = "0.13" [features] k2v = [ "garage_util/k2v", "garage_model/k2v" ] diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs new file mode 100644 index 00000000..57e3e5cf --- /dev/null +++ b/src/api/admin/api_server.rs @@ -0,0 +1,199 @@ +use std::sync::Arc; + +use async_trait::async_trait; + +use futures::future::Future; +use http::header::{ + ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW, CONTENT_TYPE, +}; +use hyper::{Body, Request, Response}; + +use opentelemetry::trace::{SpanRef, Tracer}; +use opentelemetry_prometheus::PrometheusExporter; +use prometheus::{Encoder, TextEncoder}; + +use garage_model::garage::Garage; +use garage_util::error::Error as GarageError; + +use crate::generic_server::*; + +use crate::admin::bucket::*; +use crate::admin::cluster::*; +use crate::admin::error::*; +use crate::admin::key::*; +use crate::admin::router::{Authorization, Endpoint}; + +pub struct AdminApiServer { + garage: Arc, + exporter: PrometheusExporter, + metrics_token: Option, + admin_token: Option, +} + +impl AdminApiServer { + pub fn new(garage: Arc) -> Self { + let exporter = opentelemetry_prometheus::exporter().init(); + let cfg = &garage.config.admin; + let metrics_token = cfg + .metrics_token + .as_ref() + .map(|tok| format!("Bearer {}", tok)); + let admin_token = cfg + .admin_token + .as_ref() + .map(|tok| format!("Bearer {}", tok)); + Self { + garage, + exporter, + metrics_token, + admin_token, + } + } + + pub async fn run(self, shutdown_signal: impl Future) -> Result<(), GarageError> { + if let Some(bind_addr) = self.garage.config.admin.api_bind_addr { + let region = self.garage.config.s3_api.s3_region.clone(); + ApiServer::new(region, self) + .run_server(bind_addr, shutdown_signal) + .await + } else { + Ok(()) + } + } + + fn handle_options(&self, _req: &Request) -> Result, Error> { + Ok(Response::builder() + .status(204) + .header(ALLOW, "OPTIONS, GET, POST") + .header(ACCESS_CONTROL_ALLOW_METHODS, "OPTIONS, GET, POST") + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(Body::empty())?) + } + + fn handle_metrics(&self) -> Result, Error> { + let mut buffer = vec![]; + let encoder = TextEncoder::new(); + + let tracer = opentelemetry::global::tracer("garage"); + let metric_families = tracer.in_span("admin/gather_metrics", |_| { + self.exporter.registry().gather() + }); + + encoder + .encode(&metric_families, &mut buffer) + .ok_or_internal_error("Could not serialize metrics")?; + + Ok(Response::builder() + .status(200) + .header(CONTENT_TYPE, encoder.format_type()) + .body(Body::from(buffer))?) + } +} + +#[async_trait] +impl ApiHandler for AdminApiServer { + const API_NAME: &'static str = "admin"; + const API_NAME_DISPLAY: &'static str = "Admin"; + + type Endpoint = Endpoint; + type Error = Error; + + fn parse_endpoint(&self, req: &Request) -> Result { + Endpoint::from_request(req) + } + + async fn handle( + &self, + req: Request, + endpoint: Endpoint, + ) -> Result, Error> { + let expected_auth_header = + match endpoint.authorization_type() { + Authorization::MetricsToken => self.metrics_token.as_ref(), + Authorization::AdminToken => match &self.admin_token { + None => return Err(Error::forbidden( + "Admin token isn't configured, admin API access is disabled for security.", + )), + Some(t) => Some(t), + }, + }; + + if let Some(h) = expected_auth_header { + match req.headers().get("Authorization") { + None => return Err(Error::forbidden("Authorization token must be provided")), + Some(v) => { + let authorized = v.to_str().map(|hv| hv.trim() == h).unwrap_or(false); + if !authorized { + return Err(Error::forbidden("Invalid authorization token provided")); + } + } + } + } + + match endpoint { + Endpoint::Options => self.handle_options(&req), + Endpoint::Metrics => self.handle_metrics(), + Endpoint::GetClusterStatus => handle_get_cluster_status(&self.garage).await, + Endpoint::ConnectClusterNodes => handle_connect_cluster_nodes(&self.garage, req).await, + // Layout + Endpoint::GetClusterLayout => handle_get_cluster_layout(&self.garage).await, + Endpoint::UpdateClusterLayout => handle_update_cluster_layout(&self.garage, req).await, + Endpoint::ApplyClusterLayout => handle_apply_cluster_layout(&self.garage, req).await, + Endpoint::RevertClusterLayout => handle_revert_cluster_layout(&self.garage, req).await, + // Keys + Endpoint::ListKeys => handle_list_keys(&self.garage).await, + Endpoint::GetKeyInfo { id, search } => { + handle_get_key_info(&self.garage, id, search).await + } + Endpoint::CreateKey => handle_create_key(&self.garage, req).await, + Endpoint::ImportKey => handle_import_key(&self.garage, req).await, + Endpoint::UpdateKey { id } => handle_update_key(&self.garage, id, req).await, + Endpoint::DeleteKey { id } => handle_delete_key(&self.garage, id).await, + // Buckets + Endpoint::ListBuckets => handle_list_buckets(&self.garage).await, + Endpoint::GetBucketInfo { id, global_alias } => { + handle_get_bucket_info(&self.garage, id, global_alias).await + } + Endpoint::CreateBucket => handle_create_bucket(&self.garage, req).await, + Endpoint::DeleteBucket { id } => handle_delete_bucket(&self.garage, id).await, + Endpoint::PutBucketWebsite { id } => { + handle_put_bucket_website(&self.garage, id, req).await + } + Endpoint::DeleteBucketWebsite { id } => { + handle_delete_bucket_website(&self.garage, id).await + } + // Bucket-key permissions + Endpoint::BucketAllowKey => { + handle_bucket_change_key_perm(&self.garage, req, true).await + } + Endpoint::BucketDenyKey => { + handle_bucket_change_key_perm(&self.garage, req, false).await + } + // Bucket aliasing + Endpoint::GlobalAliasBucket { id, alias } => { + handle_global_alias_bucket(&self.garage, id, alias).await + } + Endpoint::GlobalUnaliasBucket { id, alias } => { + handle_global_unalias_bucket(&self.garage, id, alias).await + } + Endpoint::LocalAliasBucket { + id, + access_key_id, + alias, + } => handle_local_alias_bucket(&self.garage, id, access_key_id, alias).await, + Endpoint::LocalUnaliasBucket { + id, + access_key_id, + alias, + } => handle_local_unalias_bucket(&self.garage, id, access_key_id, alias).await, + } + } +} + +impl ApiEndpoint for Endpoint { + fn name(&self) -> &'static str { + Endpoint::name(self) + } + + fn add_span_attributes(&self, _span: SpanRef<'_>) {} +} diff --git a/src/api/admin/bucket.rs b/src/api/admin/bucket.rs new file mode 100644 index 00000000..849d28ac --- /dev/null +++ b/src/api/admin/bucket.rs @@ -0,0 +1,549 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use hyper::{Body, Request, Response, StatusCode}; +use serde::{Deserialize, Serialize}; + +use garage_util::crdt::*; +use garage_util::data::*; +use garage_util::error::Error as GarageError; +use garage_util::time::*; + +use garage_table::*; + +use garage_model::bucket_alias_table::*; +use garage_model::bucket_table::*; +use garage_model::garage::Garage; +use garage_model::permission::*; + +use crate::admin::error::*; +use crate::admin::key::ApiBucketKeyPerm; +use crate::common_error::CommonError; +use crate::helpers::parse_json_body; + +pub async fn handle_list_buckets(garage: &Arc) -> Result, Error> { + let buckets = garage + .bucket_table + .get_range( + &EmptyKey, + None, + Some(DeletedFilter::NotDeleted), + 10000, + EnumerationOrder::Forward, + ) + .await?; + + let res = buckets + .into_iter() + .map(|b| { + let state = b.state.as_option().unwrap(); + ListBucketResultItem { + id: hex::encode(b.id), + global_aliases: state + .aliases + .items() + .iter() + .filter(|(_, _, a)| *a) + .map(|(n, _, _)| n.to_string()) + .collect::>(), + local_aliases: state + .local_aliases + .items() + .iter() + .filter(|(_, _, a)| *a) + .map(|((k, n), _, _)| BucketLocalAlias { + access_key_id: k.to_string(), + alias: n.to_string(), + }) + .collect::>(), + } + }) + .collect::>(); + + let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?; + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::from(resp_json))?) +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct ListBucketResultItem { + id: String, + global_aliases: Vec, + local_aliases: Vec, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct BucketLocalAlias { + access_key_id: String, + alias: String, +} + +pub async fn handle_get_bucket_info( + garage: &Arc, + id: Option, + global_alias: Option, +) -> Result, Error> { + let bucket_id = match (id, global_alias) { + (Some(id), None) => parse_bucket_id(&id)?, + (None, Some(ga)) => garage + .bucket_helper() + .resolve_global_bucket_name(&ga) + .await? + .ok_or_else(|| HelperError::NoSuchBucket(ga.to_string()))?, + _ => { + return Err(Error::bad_request( + "Either id or globalAlias must be provided (but not both)", + )); + } + }; + + bucket_info_results(garage, bucket_id).await +} + +async fn bucket_info_results( + garage: &Arc, + bucket_id: Uuid, +) -> Result, Error> { + let bucket = garage + .bucket_helper() + .get_existing_bucket(bucket_id) + .await?; + + let mut relevant_keys = HashMap::new(); + for (k, _) in bucket + .state + .as_option() + .unwrap() + .authorized_keys + .items() + .iter() + { + if let Some(key) = garage + .key_table + .get(&EmptyKey, k) + .await? + .filter(|k| !k.is_deleted()) + { + if !key.state.is_deleted() { + relevant_keys.insert(k.clone(), key); + } + } + } + for ((k, _), _, _) in bucket + .state + .as_option() + .unwrap() + .local_aliases + .items() + .iter() + { + if relevant_keys.contains_key(k) { + continue; + } + if let Some(key) = garage.key_table.get(&EmptyKey, k).await? { + if !key.state.is_deleted() { + relevant_keys.insert(k.clone(), key); + } + } + } + + let state = bucket.state.as_option().unwrap(); + + let res = + GetBucketInfoResult { + id: hex::encode(&bucket.id), + global_aliases: state + .aliases + .items() + .iter() + .filter(|(_, _, a)| *a) + .map(|(n, _, _)| n.to_string()) + .collect::>(), + website_access: state.website_config.get().is_some(), + website_config: state.website_config.get().clone().map(|wsc| { + GetBucketInfoWebsiteResult { + index_document: wsc.index_document, + error_document: wsc.error_document, + } + }), + keys: relevant_keys + .into_iter() + .map(|(_, key)| { + let p = key.state.as_option().unwrap(); + GetBucketInfoKey { + access_key_id: key.key_id, + name: p.name.get().to_string(), + permissions: p + .authorized_buckets + .get(&bucket.id) + .map(|p| ApiBucketKeyPerm { + read: p.allow_read, + write: p.allow_write, + owner: p.allow_owner, + }) + .unwrap_or_default(), + bucket_local_aliases: p + .local_aliases + .items() + .iter() + .filter(|(_, _, b)| *b == Some(bucket.id)) + .map(|(n, _, _)| n.to_string()) + .collect::>(), + } + }) + .collect::>(), + }; + + let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?; + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::from(resp_json))?) +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GetBucketInfoResult { + id: String, + global_aliases: Vec, + website_access: bool, + #[serde(default)] + website_config: Option, + keys: Vec, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GetBucketInfoWebsiteResult { + index_document: String, + error_document: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GetBucketInfoKey { + access_key_id: String, + name: String, + permissions: ApiBucketKeyPerm, + bucket_local_aliases: Vec, +} + +pub async fn handle_create_bucket( + garage: &Arc, + req: Request, +) -> Result, Error> { + let req = parse_json_body::(req).await?; + + if let Some(ga) = &req.global_alias { + if !is_valid_bucket_name(ga) { + return Err(Error::bad_request(format!( + "{}: {}", + ga, INVALID_BUCKET_NAME_MESSAGE + ))); + } + + if let Some(alias) = garage.bucket_alias_table.get(&EmptyKey, ga).await? { + if alias.state.get().is_some() { + return Err(CommonError::BucketAlreadyExists.into()); + } + } + } + + if let Some(la) = &req.local_alias { + if !is_valid_bucket_name(&la.alias) { + return Err(Error::bad_request(format!( + "{}: {}", + la.alias, INVALID_BUCKET_NAME_MESSAGE + ))); + } + + let key = garage + .key_helper() + .get_existing_key(&la.access_key_id) + .await?; + let state = key.state.as_option().unwrap(); + if matches!(state.local_aliases.get(&la.alias), Some(_)) { + return Err(Error::bad_request("Local alias already exists")); + } + } + + let bucket = Bucket::new(); + garage.bucket_table.insert(&bucket).await?; + + if let Some(ga) = &req.global_alias { + garage + .bucket_helper() + .set_global_bucket_alias(bucket.id, ga) + .await?; + } + + if let Some(la) = &req.local_alias { + garage + .bucket_helper() + .set_local_bucket_alias(bucket.id, &la.access_key_id, &la.alias) + .await?; + + if la.allow.read || la.allow.write || la.allow.owner { + garage + .bucket_helper() + .set_bucket_key_permissions( + bucket.id, + &la.access_key_id, + BucketKeyPerm { + timestamp: now_msec(), + allow_read: la.allow.read, + allow_write: la.allow.write, + allow_owner: la.allow.owner, + }, + ) + .await?; + } + } + + bucket_info_results(garage, bucket.id).await +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct CreateBucketRequest { + global_alias: Option, + local_alias: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct CreateBucketLocalAlias { + access_key_id: String, + alias: String, + #[serde(default)] + allow: ApiBucketKeyPerm, +} + +pub async fn handle_delete_bucket( + garage: &Arc, + id: String, +) -> Result, Error> { + let helper = garage.bucket_helper(); + + let bucket_id = parse_bucket_id(&id)?; + + let mut bucket = helper.get_existing_bucket(bucket_id).await?; + let state = bucket.state.as_option().unwrap(); + + // Check bucket is empty + if !helper.is_bucket_empty(bucket_id).await? { + return Err(CommonError::BucketNotEmpty.into()); + } + + // --- done checking, now commit --- + // 1. delete authorization from keys that had access + for (key_id, perm) in bucket.authorized_keys() { + if perm.is_any() { + helper + .set_bucket_key_permissions(bucket.id, key_id, BucketKeyPerm::NO_PERMISSIONS) + .await?; + } + } + // 2. delete all local aliases + for ((key_id, alias), _, active) in state.local_aliases.items().iter() { + if *active { + helper + .unset_local_bucket_alias(bucket.id, key_id, alias) + .await?; + } + } + // 3. delete all global aliases + for (alias, _, active) in state.aliases.items().iter() { + if *active { + helper.purge_global_bucket_alias(bucket.id, alias).await?; + } + } + + // 4. delete bucket + bucket.state = Deletable::delete(); + garage.bucket_table.insert(&bucket).await?; + + Ok(Response::builder() + .status(StatusCode::NO_CONTENT) + .body(Body::empty())?) +} + +// ---- BUCKET WEBSITE CONFIGURATION ---- + +pub async fn handle_put_bucket_website( + garage: &Arc, + id: String, + req: Request, +) -> Result, Error> { + let req = parse_json_body::(req).await?; + let bucket_id = parse_bucket_id(&id)?; + + let mut bucket = garage + .bucket_helper() + .get_existing_bucket(bucket_id) + .await?; + + let state = bucket.state.as_option_mut().unwrap(); + state.website_config.update(Some(WebsiteConfig { + index_document: req.index_document, + error_document: req.error_document, + })); + + garage.bucket_table.insert(&bucket).await?; + + bucket_info_results(garage, bucket_id).await +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct PutBucketWebsiteRequest { + index_document: String, + #[serde(default)] + error_document: Option, +} + +pub async fn handle_delete_bucket_website( + garage: &Arc, + id: String, +) -> Result, Error> { + let bucket_id = parse_bucket_id(&id)?; + + let mut bucket = garage + .bucket_helper() + .get_existing_bucket(bucket_id) + .await?; + + let state = bucket.state.as_option_mut().unwrap(); + state.website_config.update(None); + + garage.bucket_table.insert(&bucket).await?; + + bucket_info_results(garage, bucket_id).await +} + +// ---- BUCKET/KEY PERMISSIONS ---- + +pub async fn handle_bucket_change_key_perm( + garage: &Arc, + req: Request, + new_perm_flag: bool, +) -> Result, Error> { + let req = parse_json_body::(req).await?; + + let bucket_id = parse_bucket_id(&req.bucket_id)?; + + let bucket = garage + .bucket_helper() + .get_existing_bucket(bucket_id) + .await?; + let state = bucket.state.as_option().unwrap(); + + let key = garage + .key_helper() + .get_existing_key(&req.access_key_id) + .await?; + + let mut perm = state + .authorized_keys + .get(&key.key_id) + .cloned() + .unwrap_or(BucketKeyPerm::NO_PERMISSIONS); + + if req.permissions.read { + perm.allow_read = new_perm_flag; + } + if req.permissions.write { + perm.allow_write = new_perm_flag; + } + if req.permissions.owner { + perm.allow_owner = new_perm_flag; + } + + garage + .bucket_helper() + .set_bucket_key_permissions(bucket.id, &key.key_id, perm) + .await?; + + bucket_info_results(garage, bucket.id).await +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct BucketKeyPermChangeRequest { + bucket_id: String, + access_key_id: String, + permissions: ApiBucketKeyPerm, +} + +// ---- BUCKET ALIASES ---- + +pub async fn handle_global_alias_bucket( + garage: &Arc, + bucket_id: String, + alias: String, +) -> Result, Error> { + let bucket_id = parse_bucket_id(&bucket_id)?; + + garage + .bucket_helper() + .set_global_bucket_alias(bucket_id, &alias) + .await?; + + bucket_info_results(garage, bucket_id).await +} + +pub async fn handle_global_unalias_bucket( + garage: &Arc, + bucket_id: String, + alias: String, +) -> Result, Error> { + let bucket_id = parse_bucket_id(&bucket_id)?; + + garage + .bucket_helper() + .unset_global_bucket_alias(bucket_id, &alias) + .await?; + + bucket_info_results(garage, bucket_id).await +} + +pub async fn handle_local_alias_bucket( + garage: &Arc, + bucket_id: String, + access_key_id: String, + alias: String, +) -> Result, Error> { + let bucket_id = parse_bucket_id(&bucket_id)?; + + garage + .bucket_helper() + .set_local_bucket_alias(bucket_id, &access_key_id, &alias) + .await?; + + bucket_info_results(garage, bucket_id).await +} + +pub async fn handle_local_unalias_bucket( + garage: &Arc, + bucket_id: String, + access_key_id: String, + alias: String, +) -> Result, Error> { + let bucket_id = parse_bucket_id(&bucket_id)?; + + garage + .bucket_helper() + .unset_local_bucket_alias(bucket_id, &access_key_id, &alias) + .await?; + + bucket_info_results(garage, bucket_id).await +} + +// ---- HELPER ---- + +fn parse_bucket_id(id: &str) -> Result { + let id_hex = hex::decode(&id).ok_or_bad_request("Invalid bucket id")?; + Ok(Uuid::try_from(&id_hex).ok_or_bad_request("Invalid bucket id")?) +} diff --git a/src/api/admin/cluster.rs b/src/api/admin/cluster.rs new file mode 100644 index 00000000..3401be42 --- /dev/null +++ b/src/api/admin/cluster.rs @@ -0,0 +1,198 @@ +use std::collections::HashMap; +use std::net::SocketAddr; +use std::sync::Arc; + +use hyper::{Body, Request, Response, StatusCode}; +use serde::{Deserialize, Serialize}; + +use garage_util::crdt::*; +use garage_util::data::*; +use garage_util::error::Error as GarageError; + +use garage_rpc::layout::*; + +use garage_model::garage::Garage; + +use crate::admin::error::*; +use crate::helpers::parse_json_body; + +pub async fn handle_get_cluster_status(garage: &Arc) -> Result, Error> { + let res = GetClusterStatusResponse { + node: hex::encode(garage.system.id), + garage_version: garage.system.garage_version(), + known_nodes: garage + .system + .get_known_nodes() + .into_iter() + .map(|i| { + ( + hex::encode(i.id), + KnownNodeResp { + addr: i.addr, + is_up: i.is_up, + last_seen_secs_ago: i.last_seen_secs_ago, + hostname: i.status.hostname, + }, + ) + }) + .collect(), + layout: get_cluster_layout(garage), + }; + + let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?; + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::from(resp_json))?) +} + +pub async fn handle_connect_cluster_nodes( + garage: &Arc, + req: Request, +) -> Result, Error> { + let req = parse_json_body::>(req).await?; + + let res = futures::future::join_all(req.iter().map(|node| garage.system.connect(node))) + .await + .into_iter() + .map(|r| match r { + Ok(()) => ConnectClusterNodesResponse { + success: true, + error: None, + }, + Err(e) => ConnectClusterNodesResponse { + success: false, + error: Some(format!("{}", e)), + }, + }) + .collect::>(); + + let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?; + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::from(resp_json))?) +} + +pub async fn handle_get_cluster_layout(garage: &Arc) -> Result, Error> { + let res = get_cluster_layout(garage); + let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?; + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::from(resp_json))?) +} + +fn get_cluster_layout(garage: &Arc) -> GetClusterLayoutResponse { + let layout = garage.system.get_cluster_layout(); + + GetClusterLayoutResponse { + version: layout.version, + roles: layout + .roles + .items() + .iter() + .filter(|(_, _, v)| v.0.is_some()) + .map(|(k, _, v)| (hex::encode(k), v.0.clone())) + .collect(), + staged_role_changes: layout + .staging + .items() + .iter() + .filter(|(k, _, v)| layout.roles.get(k) != Some(v)) + .map(|(k, _, v)| (hex::encode(k), v.0.clone())) + .collect(), + } +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GetClusterStatusResponse { + node: String, + garage_version: &'static str, + known_nodes: HashMap, + layout: GetClusterLayoutResponse, +} + +#[derive(Serialize)] +struct ConnectClusterNodesResponse { + success: bool, + error: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GetClusterLayoutResponse { + version: u64, + roles: HashMap>, + staged_role_changes: HashMap>, +} + +#[derive(Serialize)] +struct KnownNodeResp { + addr: SocketAddr, + is_up: bool, + last_seen_secs_ago: Option, + hostname: String, +} + +pub async fn handle_update_cluster_layout( + garage: &Arc, + req: Request, +) -> Result, Error> { + let updates = parse_json_body::(req).await?; + + let mut layout = garage.system.get_cluster_layout(); + + let mut roles = layout.roles.clone(); + roles.merge(&layout.staging); + + for (node, role) in updates { + let node = hex::decode(node).ok_or_bad_request("Invalid node identifier")?; + let node = Uuid::try_from(&node).ok_or_bad_request("Invalid node identifier")?; + + layout + .staging + .merge(&roles.update_mutator(node, NodeRoleV(role))); + } + + garage.system.update_cluster_layout(&layout).await?; + + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::empty())?) +} + +pub async fn handle_apply_cluster_layout( + garage: &Arc, + req: Request, +) -> Result, Error> { + let param = parse_json_body::(req).await?; + + let layout = garage.system.get_cluster_layout(); + let layout = layout.apply_staged_changes(Some(param.version))?; + garage.system.update_cluster_layout(&layout).await?; + + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::empty())?) +} + +pub async fn handle_revert_cluster_layout( + garage: &Arc, + req: Request, +) -> Result, Error> { + let param = parse_json_body::(req).await?; + + let layout = garage.system.get_cluster_layout(); + let layout = layout.revert_staged_changes(Some(param.version))?; + garage.system.update_cluster_layout(&layout).await?; + + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::empty())?) +} + +type UpdateClusterLayoutRequest = HashMap>; + +#[derive(Deserialize)] +struct ApplyRevertLayoutRequest { + version: u64, +} diff --git a/src/api/admin/error.rs b/src/api/admin/error.rs new file mode 100644 index 00000000..c4613cb3 --- /dev/null +++ b/src/api/admin/error.rs @@ -0,0 +1,96 @@ +use err_derive::Error; +use hyper::header::HeaderValue; +use hyper::{Body, HeaderMap, StatusCode}; + +pub use garage_model::helper::error::Error as HelperError; + +use crate::common_error::CommonError; +pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInternalError}; +use crate::generic_server::ApiError; +use crate::helpers::CustomApiErrorBody; + +/// Errors of this crate +#[derive(Debug, Error)] +pub enum Error { + #[error(display = "{}", _0)] + /// Error from common error + Common(CommonError), + + // Category: cannot process + /// The API access key does not exist + #[error(display = "Access key not found: {}", _0)] + NoSuchAccessKey(String), + + /// In Import key, the key already exists + #[error( + display = "Key {} already exists in data store. Even if it is deleted, we can't let you create a new key with the same ID. Sorry.", + _0 + )] + KeyAlreadyExists(String), +} + +impl From for Error +where + CommonError: From, +{ + fn from(err: T) -> Self { + Error::Common(CommonError::from(err)) + } +} + +impl CommonErrorDerivative for Error {} + +impl From for Error { + fn from(err: HelperError) -> Self { + match err { + HelperError::Internal(i) => Self::Common(CommonError::InternalError(i)), + HelperError::BadRequest(b) => Self::Common(CommonError::BadRequest(b)), + HelperError::InvalidBucketName(n) => Self::Common(CommonError::InvalidBucketName(n)), + HelperError::NoSuchBucket(n) => Self::Common(CommonError::NoSuchBucket(n)), + HelperError::NoSuchAccessKey(n) => Self::NoSuchAccessKey(n), + } + } +} + +impl Error { + fn code(&self) -> &'static str { + match self { + Error::Common(c) => c.aws_code(), + Error::NoSuchAccessKey(_) => "NoSuchAccessKey", + Error::KeyAlreadyExists(_) => "KeyAlreadyExists", + } + } +} + +impl ApiError for Error { + /// Get the HTTP status code that best represents the meaning of the error for the client + fn http_status_code(&self) -> StatusCode { + match self { + Error::Common(c) => c.http_status_code(), + Error::NoSuchAccessKey(_) => StatusCode::NOT_FOUND, + Error::KeyAlreadyExists(_) => StatusCode::CONFLICT, + } + } + + fn add_http_headers(&self, _header_map: &mut HeaderMap) { + // nothing + } + + fn http_body(&self, garage_region: &str, path: &str) -> Body { + let error = CustomApiErrorBody { + code: self.code().to_string(), + message: format!("{}", self), + path: path.to_string(), + region: garage_region.to_string(), + }; + Body::from(serde_json::to_string_pretty(&error).unwrap_or_else(|_| { + r#" +{ + "code": "InternalError", + "message": "JSON encoding of error failed" +} + "# + .into() + })) + } +} diff --git a/src/api/admin/key.rs b/src/api/admin/key.rs new file mode 100644 index 00000000..f30b5dbb --- /dev/null +++ b/src/api/admin/key.rs @@ -0,0 +1,264 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use hyper::{Body, Request, Response, StatusCode}; +use serde::{Deserialize, Serialize}; + +use garage_util::error::Error as GarageError; + +use garage_table::*; + +use garage_model::garage::Garage; +use garage_model::key_table::*; + +use crate::admin::error::*; +use crate::helpers::parse_json_body; + +pub async fn handle_list_keys(garage: &Arc) -> Result, Error> { + let res = garage + .key_table + .get_range( + &EmptyKey, + None, + Some(KeyFilter::Deleted(DeletedFilter::NotDeleted)), + 10000, + EnumerationOrder::Forward, + ) + .await? + .iter() + .map(|k| ListKeyResultItem { + id: k.key_id.to_string(), + name: k.params().unwrap().name.get().clone(), + }) + .collect::>(); + + let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?; + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::from(resp_json))?) +} + +#[derive(Serialize)] +struct ListKeyResultItem { + id: String, + name: String, +} + +pub async fn handle_get_key_info( + garage: &Arc, + id: Option, + search: Option, +) -> Result, Error> { + let key = if let Some(id) = id { + garage.key_helper().get_existing_key(&id).await? + } else if let Some(search) = search { + garage + .key_helper() + .get_existing_matching_key(&search) + .await? + } else { + unreachable!(); + }; + + key_info_results(garage, key).await +} + +pub async fn handle_create_key( + garage: &Arc, + req: Request, +) -> Result, Error> { + let req = parse_json_body::(req).await?; + + let key = Key::new(&req.name); + garage.key_table.insert(&key).await?; + + key_info_results(garage, key).await +} + +#[derive(Deserialize)] +struct CreateKeyRequest { + name: String, +} + +pub async fn handle_import_key( + garage: &Arc, + req: Request, +) -> Result, Error> { + let req = parse_json_body::(req).await?; + + let prev_key = garage.key_table.get(&EmptyKey, &req.access_key_id).await?; + if prev_key.is_some() { + return Err(Error::KeyAlreadyExists(req.access_key_id.to_string())); + } + + let imported_key = Key::import(&req.access_key_id, &req.secret_access_key, &req.name); + garage.key_table.insert(&imported_key).await?; + + key_info_results(garage, imported_key).await +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ImportKeyRequest { + access_key_id: String, + secret_access_key: String, + name: String, +} + +pub async fn handle_update_key( + garage: &Arc, + id: String, + req: Request, +) -> Result, Error> { + let req = parse_json_body::(req).await?; + + let mut key = garage.key_helper().get_existing_key(&id).await?; + + let key_state = key.state.as_option_mut().unwrap(); + + if let Some(new_name) = req.name { + key_state.name.update(new_name); + } + if let Some(allow) = req.allow { + if allow.create_bucket { + key_state.allow_create_bucket.update(true); + } + } + if let Some(deny) = req.deny { + if deny.create_bucket { + key_state.allow_create_bucket.update(false); + } + } + + garage.key_table.insert(&key).await?; + + key_info_results(garage, key).await +} + +#[derive(Deserialize)] +struct UpdateKeyRequest { + name: Option, + allow: Option, + deny: Option, +} + +pub async fn handle_delete_key(garage: &Arc, id: String) -> Result, Error> { + let mut key = garage.key_helper().get_existing_key(&id).await?; + + key.state.as_option().unwrap(); + + garage.key_helper().delete_key(&mut key).await?; + + Ok(Response::builder() + .status(StatusCode::NO_CONTENT) + .body(Body::empty())?) +} + +async fn key_info_results(garage: &Arc, key: Key) -> Result, Error> { + let mut relevant_buckets = HashMap::new(); + + let key_state = key.state.as_option().unwrap(); + + for id in key_state + .authorized_buckets + .items() + .iter() + .map(|(id, _)| id) + .chain( + key_state + .local_aliases + .items() + .iter() + .filter_map(|(_, _, v)| v.as_ref()), + ) { + if !relevant_buckets.contains_key(id) { + if let Some(b) = garage.bucket_table.get(&EmptyKey, id).await? { + if b.state.as_option().is_some() { + relevant_buckets.insert(*id, b); + } + } + } + } + + let res = GetKeyInfoResult { + name: key_state.name.get().clone(), + access_key_id: key.key_id.clone(), + secret_access_key: key_state.secret_key.clone(), + permissions: KeyPerm { + create_bucket: *key_state.allow_create_bucket.get(), + }, + buckets: relevant_buckets + .into_iter() + .map(|(_, bucket)| { + let state = bucket.state.as_option().unwrap(); + KeyInfoBucketResult { + id: hex::encode(bucket.id), + global_aliases: state + .aliases + .items() + .iter() + .filter(|(_, _, a)| *a) + .map(|(n, _, _)| n.to_string()) + .collect::>(), + local_aliases: state + .local_aliases + .items() + .iter() + .filter(|((k, _), _, a)| *a && *k == key.key_id) + .map(|((_, n), _, _)| n.to_string()) + .collect::>(), + permissions: key_state + .authorized_buckets + .get(&bucket.id) + .map(|p| ApiBucketKeyPerm { + read: p.allow_read, + write: p.allow_write, + owner: p.allow_owner, + }) + .unwrap_or_default(), + } + }) + .collect::>(), + }; + + let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?; + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::from(resp_json))?) +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GetKeyInfoResult { + name: String, + access_key_id: String, + secret_access_key: String, + permissions: KeyPerm, + buckets: Vec, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct KeyPerm { + #[serde(default)] + create_bucket: bool, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct KeyInfoBucketResult { + id: String, + global_aliases: Vec, + local_aliases: Vec, + permissions: ApiBucketKeyPerm, +} + +#[derive(Serialize, Deserialize, Default)] +pub(crate) struct ApiBucketKeyPerm { + #[serde(default)] + pub(crate) read: bool, + #[serde(default)] + pub(crate) write: bool, + #[serde(default)] + pub(crate) owner: bool, +} diff --git a/src/api/admin/mod.rs b/src/api/admin/mod.rs new file mode 100644 index 00000000..c4857c10 --- /dev/null +++ b/src/api/admin/mod.rs @@ -0,0 +1,7 @@ +pub mod api_server; +mod error; +mod router; + +mod bucket; +mod cluster; +mod key; diff --git a/src/api/admin/router.rs b/src/api/admin/router.rs new file mode 100644 index 00000000..93639873 --- /dev/null +++ b/src/api/admin/router.rs @@ -0,0 +1,149 @@ +use std::borrow::Cow; + +use hyper::{Method, Request}; + +use crate::admin::error::*; +use crate::router_macros::*; + +pub enum Authorization { + MetricsToken, + AdminToken, +} + +router_match! {@func + +/// List of all Admin API endpoints. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Endpoint { + Options, + Metrics, + GetClusterStatus, + ConnectClusterNodes, + // Layout + GetClusterLayout, + UpdateClusterLayout, + ApplyClusterLayout, + RevertClusterLayout, + // Keys + ListKeys, + CreateKey, + ImportKey, + GetKeyInfo { + id: Option, + search: Option, + }, + DeleteKey { + id: String, + }, + UpdateKey { + id: String, + }, + // Buckets + ListBuckets, + CreateBucket, + GetBucketInfo { + id: Option, + global_alias: Option, + }, + DeleteBucket { + id: String, + }, + PutBucketWebsite { + id: String, + }, + DeleteBucketWebsite { + id: String, + }, + // Bucket-Key Permissions + BucketAllowKey, + BucketDenyKey, + // Bucket aliases + GlobalAliasBucket { + id: String, + alias: String, + }, + GlobalUnaliasBucket { + id: String, + alias: String, + }, + LocalAliasBucket { + id: String, + access_key_id: String, + alias: String, + }, + LocalUnaliasBucket { + id: String, + access_key_id: String, + alias: String, + }, +}} + +impl Endpoint { + /// Determine which S3 endpoint a request is for using the request, and a bucket which was + /// possibly extracted from the Host header. + /// Returns Self plus bucket name, if endpoint is not Endpoint::ListBuckets + pub fn from_request(req: &Request) -> Result { + let uri = req.uri(); + let path = uri.path(); + let query = uri.query(); + + let mut query = QueryParameters::from_query(query.unwrap_or_default())?; + + let res = router_match!(@gen_path_parser (req.method(), path, query) [ + OPTIONS _ => Options, + GET "/metrics" => Metrics, + GET "/v0/status" => GetClusterStatus, + POST "/v0/connect" => ConnectClusterNodes, + // Layout endpoints + GET "/v0/layout" => GetClusterLayout, + POST "/v0/layout" => UpdateClusterLayout, + POST "/v0/layout/apply" => ApplyClusterLayout, + POST "/v0/layout/revert" => RevertClusterLayout, + // API key endpoints + GET "/v0/key" if id => GetKeyInfo (query_opt::id, query_opt::search), + GET "/v0/key" if search => GetKeyInfo (query_opt::id, query_opt::search), + POST "/v0/key" if id => UpdateKey (query::id), + POST "/v0/key" => CreateKey, + POST "/v0/key/import" => ImportKey, + DELETE "/v0/key" if id => DeleteKey (query::id), + GET "/v0/key" => ListKeys, + // Bucket endpoints + GET "/v0/bucket" if id => GetBucketInfo (query_opt::id, query_opt::global_alias), + GET "/v0/bucket" if global_alias => GetBucketInfo (query_opt::id, query_opt::global_alias), + GET "/v0/bucket" => ListBuckets, + POST "/v0/bucket" => CreateBucket, + DELETE "/v0/bucket" if id => DeleteBucket (query::id), + PUT "/v0/bucket/website" if id => PutBucketWebsite (query::id), + DELETE "/v0/bucket/website" if id => DeleteBucketWebsite (query::id), + // Bucket-key permissions + POST "/v0/bucket/allow" => BucketAllowKey, + POST "/v0/bucket/deny" => BucketDenyKey, + // Bucket aliases + PUT "/v0/bucket/alias/global" => GlobalAliasBucket (query::id, query::alias), + DELETE "/v0/bucket/alias/global" => GlobalUnaliasBucket (query::id, query::alias), + PUT "/v0/bucket/alias/local" => LocalAliasBucket (query::id, query::access_key_id, query::alias), + DELETE "/v0/bucket/alias/local" => LocalUnaliasBucket (query::id, query::access_key_id, query::alias), + ]); + + if let Some(message) = query.nonempty_message() { + debug!("Unused query parameter: {}", message) + } + + Ok(res) + } + /// Get the kind of authorization which is required to perform the operation. + pub fn authorization_type(&self) -> Authorization { + match self { + Self::Metrics => Authorization::MetricsToken, + _ => Authorization::AdminToken, + } + } +} + +generateQueryParameters! { + "id" => id, + "search" => search, + "globalAlias" => global_alias, + "alias" => alias, + "accessKeyId" => access_key_id +} diff --git a/src/api/common_error.rs b/src/api/common_error.rs new file mode 100644 index 00000000..20f9f266 --- /dev/null +++ b/src/api/common_error.rs @@ -0,0 +1,177 @@ +use err_derive::Error; +use hyper::StatusCode; + +use garage_util::error::Error as GarageError; + +/// Errors of this crate +#[derive(Debug, Error)] +pub enum CommonError { + // ---- INTERNAL ERRORS ---- + /// Error related to deeper parts of Garage + #[error(display = "Internal error: {}", _0)] + InternalError(#[error(source)] GarageError), + + /// Error related to Hyper + #[error(display = "Internal error (Hyper error): {}", _0)] + Hyper(#[error(source)] hyper::Error), + + /// Error related to HTTP + #[error(display = "Internal error (HTTP error): {}", _0)] + Http(#[error(source)] http::Error), + + // ---- GENERIC CLIENT ERRORS ---- + /// Proper authentication was not provided + #[error(display = "Forbidden: {}", _0)] + Forbidden(String), + + /// Generic bad request response with custom message + #[error(display = "Bad request: {}", _0)] + BadRequest(String), + + // ---- SPECIFIC ERROR CONDITIONS ---- + // These have to be error codes referenced in the S3 spec here: + // https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html#ErrorCodeList + /// The bucket requested don't exists + #[error(display = "Bucket not found: {}", _0)] + NoSuchBucket(String), + + /// Tried to create a bucket that already exist + #[error(display = "Bucket already exists")] + BucketAlreadyExists, + + /// Tried to delete a non-empty bucket + #[error(display = "Tried to delete a non-empty bucket")] + BucketNotEmpty, + + // Category: bad request + /// Bucket name is not valid according to AWS S3 specs + #[error(display = "Invalid bucket name: {}", _0)] + InvalidBucketName(String), +} + +impl CommonError { + pub fn http_status_code(&self) -> StatusCode { + match self { + CommonError::InternalError( + GarageError::Timeout + | GarageError::RemoteError(_) + | GarageError::Quorum(_, _, _, _), + ) => StatusCode::SERVICE_UNAVAILABLE, + CommonError::InternalError(_) | CommonError::Hyper(_) | CommonError::Http(_) => { + StatusCode::INTERNAL_SERVER_ERROR + } + CommonError::BadRequest(_) => StatusCode::BAD_REQUEST, + CommonError::Forbidden(_) => StatusCode::FORBIDDEN, + CommonError::NoSuchBucket(_) => StatusCode::NOT_FOUND, + CommonError::BucketNotEmpty | CommonError::BucketAlreadyExists => StatusCode::CONFLICT, + CommonError::InvalidBucketName(_) => StatusCode::BAD_REQUEST, + } + } + + pub fn aws_code(&self) -> &'static str { + match self { + CommonError::Forbidden(_) => "AccessDenied", + CommonError::InternalError( + GarageError::Timeout + | GarageError::RemoteError(_) + | GarageError::Quorum(_, _, _, _), + ) => "ServiceUnavailable", + CommonError::InternalError(_) | CommonError::Hyper(_) | CommonError::Http(_) => { + "InternalError" + } + CommonError::BadRequest(_) => "InvalidRequest", + CommonError::NoSuchBucket(_) => "NoSuchBucket", + CommonError::BucketAlreadyExists => "BucketAlreadyExists", + CommonError::BucketNotEmpty => "BucketNotEmpty", + CommonError::InvalidBucketName(_) => "InvalidBucketName", + } + } + + pub fn bad_request(msg: M) -> Self { + CommonError::BadRequest(msg.to_string()) + } +} + +pub trait CommonErrorDerivative: From { + fn internal_error(msg: M) -> Self { + Self::from(CommonError::InternalError(GarageError::Message( + msg.to_string(), + ))) + } + + fn bad_request(msg: M) -> Self { + Self::from(CommonError::BadRequest(msg.to_string())) + } + + fn forbidden(msg: M) -> Self { + Self::from(CommonError::Forbidden(msg.to_string())) + } +} + +/// Trait to map error to the Bad Request error code +pub trait OkOrBadRequest { + type S; + fn ok_or_bad_request>(self, reason: M) -> Result; +} + +impl OkOrBadRequest for Result +where + E: std::fmt::Display, +{ + type S = T; + fn ok_or_bad_request>(self, reason: M) -> Result { + match self { + Ok(x) => Ok(x), + Err(e) => Err(CommonError::BadRequest(format!( + "{}: {}", + reason.as_ref(), + e + ))), + } + } +} + +impl OkOrBadRequest for Option { + type S = T; + fn ok_or_bad_request>(self, reason: M) -> Result { + match self { + Some(x) => Ok(x), + None => Err(CommonError::BadRequest(reason.as_ref().to_string())), + } + } +} + +/// Trait to map an error to an Internal Error code +pub trait OkOrInternalError { + type S; + fn ok_or_internal_error>(self, reason: M) -> Result; +} + +impl OkOrInternalError for Result +where + E: std::fmt::Display, +{ + type S = T; + fn ok_or_internal_error>(self, reason: M) -> Result { + match self { + Ok(x) => Ok(x), + Err(e) => Err(CommonError::InternalError(GarageError::Message(format!( + "{}: {}", + reason.as_ref(), + e + )))), + } + } +} + +impl OkOrInternalError for Option { + type S = T; + fn ok_or_internal_error>(self, reason: M) -> Result { + match self { + Some(x) => Ok(x), + None => Err(CommonError::InternalError(GarageError::Message( + reason.as_ref().to_string(), + ))), + } + } +} diff --git a/src/api/generic_server.rs b/src/api/generic_server.rs index 9281e596..77278908 100644 --- a/src/api/generic_server.rs +++ b/src/api/generic_server.rs @@ -5,9 +5,11 @@ use async_trait::async_trait; use futures::future::Future; +use hyper::header::HeaderValue; use hyper::server::conn::AddrStream; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Request, Response, Server}; +use hyper::{HeaderMap, StatusCode}; use opentelemetry::{ global, @@ -19,26 +21,31 @@ use opentelemetry::{ use garage_util::error::Error as GarageError; use garage_util::metrics::{gen_trace_id, RecordDuration}; -use crate::error::*; - pub(crate) trait ApiEndpoint: Send + Sync + 'static { fn name(&self) -> &'static str; fn add_span_attributes(&self, span: SpanRef<'_>); } +pub trait ApiError: std::error::Error + Send + Sync + 'static { + fn http_status_code(&self) -> StatusCode; + fn add_http_headers(&self, header_map: &mut HeaderMap); + fn http_body(&self, garage_region: &str, path: &str) -> Body; +} + #[async_trait] pub(crate) trait ApiHandler: Send + Sync + 'static { const API_NAME: &'static str; const API_NAME_DISPLAY: &'static str; type Endpoint: ApiEndpoint; + type Error: ApiError; - fn parse_endpoint(&self, r: &Request) -> Result; + fn parse_endpoint(&self, r: &Request) -> Result; async fn handle( &self, req: Request, endpoint: Self::Endpoint, - ) -> Result, Error>; + ) -> Result, Self::Error>; } pub(crate) struct ApiServer { @@ -142,13 +149,13 @@ impl ApiServer { Ok(x) } Err(e) => { - let body: Body = Body::from(e.aws_xml(&self.region, uri.path())); + let body: Body = e.http_body(&self.region, uri.path()); let mut http_error_builder = Response::builder() .status(e.http_status_code()) .header("Content-Type", "application/xml"); if let Some(header_map) = http_error_builder.headers_mut() { - e.add_headers(header_map) + e.add_http_headers(header_map) } let http_error = http_error_builder.body(body)?; @@ -163,7 +170,7 @@ impl ApiServer { } } - async fn handler_stage2(&self, req: Request) -> Result, Error> { + async fn handler_stage2(&self, req: Request) -> Result, A::Error> { let endpoint = self.api_handler.parse_endpoint(&req)?; debug!("Endpoint: {}", endpoint.name()); diff --git a/src/api/helpers.rs b/src/api/helpers.rs index a994b82f..9fb12dbe 100644 --- a/src/api/helpers.rs +++ b/src/api/helpers.rs @@ -1,11 +1,8 @@ +use hyper::{Body, Request}; use idna::domain_to_unicode; +use serde::{Deserialize, Serialize}; -use garage_util::data::*; - -use garage_model::garage::Garage; -use garage_model::key_table::Key; - -use crate::error::*; +use crate::common_error::{CommonError as Error, *}; /// What kind of authorization is required to perform a given action #[derive(Debug, Clone, PartialEq, Eq)] @@ -50,7 +47,7 @@ pub fn authority_to_host(authority: &str) -> Result { let mut iter = authority.chars().enumerate(); let (_, first_char) = iter .next() - .ok_or_else(|| Error::BadRequest("Authority is empty".to_string()))?; + .ok_or_else(|| Error::bad_request("Authority is empty".to_string()))?; let split = match first_char { '[' => { @@ -58,7 +55,7 @@ pub fn authority_to_host(authority: &str) -> Result { match iter.next() { Some((_, ']')) => iter.next(), _ => { - return Err(Error::BadRequest(format!( + return Err(Error::bad_request(format!( "Authority {} has an illegal format", authority ))) @@ -71,7 +68,7 @@ pub fn authority_to_host(authority: &str) -> Result { let authority = match split { Some((i, ':')) => Ok(&authority[..i]), None => Ok(authority), - Some((_, _)) => Err(Error::BadRequest(format!( + Some((_, _)) => Err(Error::bad_request(format!( "Authority {} has an illegal format", authority ))), @@ -79,28 +76,6 @@ pub fn authority_to_host(authority: &str) -> Result { authority.map(|h| domain_to_unicode(h).0) } -#[allow(clippy::ptr_arg)] -pub async fn resolve_bucket( - garage: &Garage, - bucket_name: &String, - api_key: &Key, -) -> Result { - let api_key_params = api_key - .state - .as_option() - .ok_or_internal_error("Key should not be deleted at this point")?; - - if let Some(Some(bucket_id)) = api_key_params.local_aliases.get(bucket_name) { - Ok(*bucket_id) - } else { - Ok(garage - .bucket_helper() - .resolve_global_bucket_name(bucket_name) - .await? - .ok_or(Error::NoSuchBucket)?) - } -} - /// Extract the bucket name and the key name from an HTTP path and possibly a bucket provided in /// the host header of the request /// @@ -132,7 +107,7 @@ pub fn parse_bucket_key<'a>( None => (path, None), }; if bucket.is_empty() { - return Err(Error::BadRequest("No bucket specified".to_string())); + return Err(Error::bad_request("No bucket specified")); } Ok((bucket, key)) } @@ -163,6 +138,12 @@ pub fn key_after_prefix(pfx: &str) -> Option { None } +pub async fn parse_json_body Deserialize<'de>>(req: Request) -> Result { + let body = hyper::body::to_bytes(req.into_body()).await?; + let resp: T = serde_json::from_slice(&body).ok_or_bad_request("Invalid JSON")?; + Ok(resp) +} + #[cfg(test)] mod tests { use super::*; @@ -298,3 +279,11 @@ mod tests { ); } } + +#[derive(Serialize)] +pub(crate) struct CustomApiErrorBody { + pub(crate) code: String, + pub(crate) message: String, + pub(crate) region: String, + pub(crate) path: String, +} diff --git a/src/api/k2v/api_server.rs b/src/api/k2v/api_server.rs index 5f5e9030..eb0fbdd7 100644 --- a/src/api/k2v/api_server.rs +++ b/src/api/k2v/api_server.rs @@ -7,13 +7,12 @@ use hyper::{Body, Method, Request, Response}; use opentelemetry::{trace::SpanRef, KeyValue}; -use garage_table::util::*; use garage_util::error::Error as GarageError; use garage_model::garage::Garage; -use crate::error::*; use crate::generic_server::*; +use crate::k2v::error::*; use crate::signature::payload::check_payload_signature; use crate::signature::streaming::*; @@ -60,6 +59,7 @@ impl ApiHandler for K2VApiServer { const API_NAME_DISPLAY: &'static str = "K2V"; type Endpoint = K2VApiEndpoint; + type Error = Error; fn parse_endpoint(&self, req: &Request) -> Result { let (endpoint, bucket_name) = Endpoint::from_request(req)?; @@ -83,13 +83,14 @@ impl ApiHandler for K2VApiServer { // The OPTIONS method is procesed early, before we even check for an API key if let Endpoint::Options = endpoint { - return handle_options_s3api(garage, &req, Some(bucket_name)).await; + return Ok(handle_options_s3api(garage, &req, Some(bucket_name)) + .await + .ok_or_bad_request("Error handling OPTIONS")?); } let (api_key, mut content_sha256) = check_payload_signature(&garage, "k2v", &req).await?; - let api_key = api_key.ok_or_else(|| { - Error::Forbidden("Garage does not support anonymous access yet".to_string()) - })?; + let api_key = api_key + .ok_or_else(|| Error::forbidden("Garage does not support anonymous access yet"))?; let req = parse_streaming_body( &api_key, @@ -99,13 +100,14 @@ impl ApiHandler for K2VApiServer { "k2v", )?; - let bucket_id = resolve_bucket(&garage, &bucket_name, &api_key).await?; + let bucket_id = garage + .bucket_helper() + .resolve_bucket(&bucket_name, &api_key) + .await?; let bucket = garage - .bucket_table - .get(&EmptyKey, &bucket_id) - .await? - .filter(|b| !b.state.is_deleted()) - .ok_or(Error::NoSuchBucket)?; + .bucket_helper() + .get_existing_bucket(bucket_id) + .await?; let allowed = match endpoint.authorization_type() { Authorization::Read => api_key.allow_read(&bucket_id), @@ -115,9 +117,7 @@ impl ApiHandler for K2VApiServer { }; if !allowed { - return Err(Error::Forbidden( - "Operation is not allowed for this key.".to_string(), - )); + return Err(Error::forbidden("Operation is not allowed for this key.")); } // Look up what CORS rule might apply to response. @@ -125,7 +125,8 @@ impl ApiHandler for K2VApiServer { // are always preflighted, i.e. the browser should make // an OPTIONS call before to check it is allowed let matching_cors_rule = match *req.method() { - Method::GET | Method::HEAD | Method::POST => find_matching_cors_rule(&bucket, &req)?, + Method::GET | Method::HEAD | Method::POST => find_matching_cors_rule(&bucket, &req) + .ok_or_internal_error("Error looking up CORS rule")?, _ => None, }; diff --git a/src/api/k2v/batch.rs b/src/api/k2v/batch.rs index 4ecddeb9..db9901cf 100644 --- a/src/api/k2v/batch.rs +++ b/src/api/k2v/batch.rs @@ -12,7 +12,8 @@ use garage_model::garage::Garage; use garage_model::k2v::causality::*; use garage_model::k2v::item_table::*; -use crate::error::*; +use crate::helpers::*; +use crate::k2v::error::*; use crate::k2v::range::read_range; pub async fn handle_insert_batch( @@ -20,9 +21,7 @@ pub async fn handle_insert_batch( bucket_id: Uuid, req: Request, ) -> Result, Error> { - let body = hyper::body::to_bytes(req.into_body()).await?; - let items: Vec = - serde_json::from_slice(&body).ok_or_bad_request("Invalid JSON")?; + let items = parse_json_body::>(req).await?; let mut items2 = vec![]; for it in items { @@ -52,9 +51,7 @@ pub async fn handle_read_batch( bucket_id: Uuid, req: Request, ) -> Result, Error> { - let body = hyper::body::to_bytes(req.into_body()).await?; - let queries: Vec = - serde_json::from_slice(&body).ok_or_bad_request("Invalid JSON")?; + let queries = parse_json_body::>(req).await?; let resp_results = futures::future::join_all( queries @@ -91,7 +88,7 @@ async fn handle_read_batch_query( let (items, more, next_start) = if query.single_item { if query.prefix.is_some() || query.end.is_some() || query.limit.is_some() || query.reverse { - return Err(Error::BadRequest("Batch query parameters 'prefix', 'end', 'limit' and 'reverse' must not be set when singleItem is true.".into())); + return Err(Error::bad_request("Batch query parameters 'prefix', 'end', 'limit' and 'reverse' must not be set when singleItem is true.")); } let sk = query .start @@ -149,9 +146,7 @@ pub async fn handle_delete_batch( bucket_id: Uuid, req: Request, ) -> Result, Error> { - let body = hyper::body::to_bytes(req.into_body()).await?; - let queries: Vec = - serde_json::from_slice(&body).ok_or_bad_request("Invalid JSON")?; + let queries = parse_json_body::>(req).await?; let resp_results = futures::future::join_all( queries @@ -188,7 +183,7 @@ async fn handle_delete_batch_query( let deleted_items = if query.single_item { if query.prefix.is_some() || query.end.is_some() { - return Err(Error::BadRequest("Batch query parameters 'prefix' and 'end' must not be set when singleItem is true.".into())); + return Err(Error::bad_request("Batch query parameters 'prefix' and 'end' must not be set when singleItem is true.")); } let sk = query .start diff --git a/src/api/k2v/error.rs b/src/api/k2v/error.rs new file mode 100644 index 00000000..4c55d8b5 --- /dev/null +++ b/src/api/k2v/error.rs @@ -0,0 +1,134 @@ +use err_derive::Error; +use hyper::header::HeaderValue; +use hyper::{Body, HeaderMap, StatusCode}; + +use garage_model::helper::error::Error as HelperError; + +use crate::common_error::CommonError; +pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInternalError}; +use crate::generic_server::ApiError; +use crate::helpers::CustomApiErrorBody; +use crate::signature::error::Error as SignatureError; + +/// Errors of this crate +#[derive(Debug, Error)] +pub enum Error { + #[error(display = "{}", _0)] + /// Error from common error + Common(CommonError), + + // Category: cannot process + /// Authorization Header Malformed + #[error(display = "Authorization header malformed, expected scope: {}", _0)] + AuthorizationHeaderMalformed(String), + + /// The object requested don't exists + #[error(display = "Key not found")] + NoSuchKey, + + /// Some base64 encoded data was badly encoded + #[error(display = "Invalid base64: {}", _0)] + InvalidBase64(#[error(source)] base64::DecodeError), + + /// The client sent a header with invalid value + #[error(display = "Invalid header value: {}", _0)] + InvalidHeader(#[error(source)] hyper::header::ToStrError), + + /// The client asked for an invalid return format (invalid Accept header) + #[error(display = "Not acceptable: {}", _0)] + NotAcceptable(String), + + /// The request contained an invalid UTF-8 sequence in its path or in other parameters + #[error(display = "Invalid UTF-8: {}", _0)] + InvalidUtf8Str(#[error(source)] std::str::Utf8Error), +} + +impl From for Error +where + CommonError: From, +{ + fn from(err: T) -> Self { + Error::Common(CommonError::from(err)) + } +} + +impl CommonErrorDerivative for Error {} + +impl From for Error { + fn from(err: HelperError) -> Self { + match err { + HelperError::Internal(i) => Self::Common(CommonError::InternalError(i)), + HelperError::BadRequest(b) => Self::Common(CommonError::BadRequest(b)), + HelperError::InvalidBucketName(n) => Self::Common(CommonError::InvalidBucketName(n)), + HelperError::NoSuchBucket(n) => Self::Common(CommonError::NoSuchBucket(n)), + e => Self::Common(CommonError::BadRequest(format!("{}", e))), + } + } +} + +impl From for Error { + fn from(err: SignatureError) -> Self { + match err { + SignatureError::Common(c) => Self::Common(c), + SignatureError::AuthorizationHeaderMalformed(c) => { + Self::AuthorizationHeaderMalformed(c) + } + SignatureError::InvalidUtf8Str(i) => Self::InvalidUtf8Str(i), + SignatureError::InvalidHeader(h) => Self::InvalidHeader(h), + } + } +} + +impl Error { + /// This returns a keyword for the corresponding error. + /// Here, these keywords are not necessarily those from AWS S3, + /// as we are building a custom API + fn code(&self) -> &'static str { + match self { + Error::Common(c) => c.aws_code(), + Error::NoSuchKey => "NoSuchKey", + Error::NotAcceptable(_) => "NotAcceptable", + Error::AuthorizationHeaderMalformed(_) => "AuthorizationHeaderMalformed", + Error::InvalidBase64(_) => "InvalidBase64", + Error::InvalidHeader(_) => "InvalidHeaderValue", + Error::InvalidUtf8Str(_) => "InvalidUtf8String", + } + } +} + +impl ApiError for Error { + /// Get the HTTP status code that best represents the meaning of the error for the client + fn http_status_code(&self) -> StatusCode { + match self { + Error::Common(c) => c.http_status_code(), + Error::NoSuchKey => StatusCode::NOT_FOUND, + Error::NotAcceptable(_) => StatusCode::NOT_ACCEPTABLE, + Error::AuthorizationHeaderMalformed(_) + | Error::InvalidBase64(_) + | Error::InvalidHeader(_) + | Error::InvalidUtf8Str(_) => StatusCode::BAD_REQUEST, + } + } + + fn add_http_headers(&self, _header_map: &mut HeaderMap) { + // nothing + } + + fn http_body(&self, garage_region: &str, path: &str) -> Body { + let error = CustomApiErrorBody { + code: self.code().to_string(), + message: format!("{}", self), + path: path.to_string(), + region: garage_region.to_string(), + }; + Body::from(serde_json::to_string_pretty(&error).unwrap_or_else(|_| { + r#" +{ + "code": "InternalError", + "message": "JSON encoding of error failed" +} + "# + .into() + })) + } +} diff --git a/src/api/k2v/index.rs b/src/api/k2v/index.rs index 896dbcf0..d5db906d 100644 --- a/src/api/k2v/index.rs +++ b/src/api/k2v/index.rs @@ -12,7 +12,7 @@ use garage_table::util::*; use garage_model::garage::Garage; use garage_model::k2v::counter_table::{BYTES, CONFLICTS, ENTRIES, VALUES}; -use crate::error::*; +use crate::k2v::error::*; use crate::k2v::range::read_range; pub async fn handle_read_index( diff --git a/src/api/k2v/item.rs b/src/api/k2v/item.rs index 1860863e..836d386f 100644 --- a/src/api/k2v/item.rs +++ b/src/api/k2v/item.rs @@ -10,7 +10,7 @@ use garage_model::garage::Garage; use garage_model::k2v::causality::*; use garage_model::k2v::item_table::*; -use crate::error::*; +use crate::k2v::error::*; pub const X_GARAGE_CAUSALITY_TOKEN: &str = "X-Garage-Causality-Token"; diff --git a/src/api/k2v/mod.rs b/src/api/k2v/mod.rs index ee210ad5..b6a8c5cf 100644 --- a/src/api/k2v/mod.rs +++ b/src/api/k2v/mod.rs @@ -1,4 +1,5 @@ pub mod api_server; +mod error; mod router; mod batch; diff --git a/src/api/k2v/range.rs b/src/api/k2v/range.rs index 295c34aa..bb9d3be5 100644 --- a/src/api/k2v/range.rs +++ b/src/api/k2v/range.rs @@ -7,8 +7,8 @@ use std::sync::Arc; use garage_table::replication::TableShardedReplication; use garage_table::*; -use crate::error::*; use crate::helpers::key_after_prefix; +use crate::k2v::error::*; /// Read range in a Garage table. /// Returns (entries, more?, nextStart) @@ -31,7 +31,7 @@ where (None, Some(s)) => (Some(s.clone()), false), (Some(p), Some(s)) => { if !s.starts_with(p) { - return Err(Error::BadRequest(format!( + return Err(Error::bad_request(format!( "Start key '{}' does not start with prefix '{}'", s, p ))); diff --git a/src/api/k2v/router.rs b/src/api/k2v/router.rs index f948ffce..50e6965b 100644 --- a/src/api/k2v/router.rs +++ b/src/api/k2v/router.rs @@ -1,4 +1,4 @@ -use crate::error::*; +use crate::k2v::error::*; use std::borrow::Cow; @@ -62,7 +62,7 @@ impl Endpoint { .unwrap_or((path.to_owned(), "")); if bucket.is_empty() { - return Err(Error::BadRequest("Missing bucket name".to_owned())); + return Err(Error::bad_request("Missing bucket name")); } if *req.method() == Method::OPTIONS { @@ -83,7 +83,7 @@ impl Endpoint { Method::PUT => Self::from_put(partition_key, &mut query)?, Method::DELETE => Self::from_delete(partition_key, &mut query)?, _ if req.method() == method_search => Self::from_search(partition_key, &mut query)?, - _ => return Err(Error::BadRequest("Unknown method".to_owned())), + _ => return Err(Error::bad_request("Unknown method")), }; if let Some(message) = query.nonempty_message() { diff --git a/src/api/lib.rs b/src/api/lib.rs index 0078f7b5..370dfd7a 100644 --- a/src/api/lib.rs +++ b/src/api/lib.rs @@ -2,16 +2,16 @@ #[macro_use] extern crate tracing; -pub mod error; -pub use error::Error; +pub mod common_error; mod encoding; -mod generic_server; +pub mod generic_server; pub mod helpers; mod router_macros; /// This mode is public only to help testing. Don't expect stability here pub mod signature; +pub mod admin; #[cfg(feature = "k2v")] pub mod k2v; pub mod s3; diff --git a/src/api/router_macros.rs b/src/api/router_macros.rs index 8471407c..4c593300 100644 --- a/src/api/router_macros.rs +++ b/src/api/router_macros.rs @@ -23,6 +23,29 @@ macro_rules! router_match { _ => None } }}; + (@gen_path_parser ($method:expr, $reqpath:expr, $query:expr) + [ + $($meth:ident $path:pat $(if $required:ident)? => $api:ident $(($($conv:ident :: $param:ident),*))?,)* + ]) => {{ + { + use Endpoint::*; + match ($method, $reqpath) { + $( + (&Method::$meth, $path) if true $(&& $query.$required.is_some())? => $api { + $($( + $param: router_match!(@@parse_param $query, $conv, $param), + )*)? + }, + )* + (m, p) => { + return Err(Error::bad_request(format!( + "Unknown API endpoint: {} {}", + m, p + ))) + } + } + } + }}; (@gen_parser ($keyword:expr, $key:ident, $query:expr, $header:expr), key: [$($kw_k:ident $(if $required_k:ident)? $(header $header_k:expr)? => $api_k:ident $(($($conv_k:ident :: $param_k:ident),*))?,)*], no_key: [$($kw_nk:ident $(if $required_nk:ident)? $(if_header $header_nk:expr)? => $api_nk:ident $(($($conv_nk:ident :: $param_nk:ident),*))?,)*]) => {{ @@ -55,7 +78,7 @@ macro_rules! router_match { )*)? }), )* - (kw, _) => Err(Error::BadRequest(format!("Invalid endpoint: {}", kw))) + (kw, _) => Err(Error::bad_request(format!("Invalid endpoint: {}", kw))) } }}; @@ -74,14 +97,14 @@ macro_rules! router_match { .take() .map(|param| param.parse()) .transpose() - .map_err(|_| Error::BadRequest("Failed to parse query parameter".to_owned()))? + .map_err(|_| Error::bad_request("Failed to parse query parameter"))? }}; (@@parse_param $query:expr, parse, $param:ident) => {{ // extract and parse mandatory query parameter // both missing and un-parseable parameters are reported as errors $query.$param.take().ok_or_bad_request("Missing argument for endpoint")? .parse() - .map_err(|_| Error::BadRequest("Failed to parse query parameter".to_owned()))? + .map_err(|_| Error::bad_request("Failed to parse query parameter"))? }}; (@func $(#[$doc:meta])* @@ -150,7 +173,7 @@ macro_rules! generateQueryParameters { false } else if v.as_ref().is_empty() { if res.keyword.replace(k).is_some() { - return Err(Error::BadRequest("Multiple keywords".to_owned())); + return Err(Error::bad_request("Multiple keywords")); } continue; } else { @@ -160,7 +183,7 @@ macro_rules! generateQueryParameters { } }; if repeated { - return Err(Error::BadRequest(format!( + return Err(Error::bad_request(format!( "Query parameter repeated: '{}'", k ))); diff --git a/src/api/s3/api_server.rs b/src/api/s3/api_server.rs index 78a69d53..ecc417ab 100644 --- a/src/api/s3/api_server.rs +++ b/src/api/s3/api_server.rs @@ -8,14 +8,13 @@ use hyper::{Body, Method, Request, Response}; use opentelemetry::{trace::SpanRef, KeyValue}; -use garage_table::util::*; use garage_util::error::Error as GarageError; use garage_model::garage::Garage; use garage_model::key_table::Key; -use crate::error::*; use crate::generic_server::*; +use crate::s3::error::*; use crate::signature::payload::check_payload_signature; use crate::signature::streaming::*; @@ -75,6 +74,7 @@ impl ApiHandler for S3ApiServer { const API_NAME_DISPLAY: &'static str = "S3"; type Endpoint = S3ApiEndpoint; + type Error = Error; fn parse_endpoint(&self, req: &Request) -> Result { let authority = req @@ -122,9 +122,8 @@ impl ApiHandler for S3ApiServer { } let (api_key, mut content_sha256) = check_payload_signature(&garage, "s3", &req).await?; - let api_key = api_key.ok_or_else(|| { - Error::Forbidden("Garage does not support anonymous access yet".to_string()) - })?; + let api_key = api_key + .ok_or_else(|| Error::forbidden("Garage does not support anonymous access yet"))?; let req = parse_streaming_body( &api_key, @@ -148,13 +147,14 @@ impl ApiHandler for S3ApiServer { return handle_create_bucket(&garage, req, content_sha256, api_key, bucket_name).await; } - let bucket_id = resolve_bucket(&garage, &bucket_name, &api_key).await?; + let bucket_id = garage + .bucket_helper() + .resolve_bucket(&bucket_name, &api_key) + .await?; let bucket = garage - .bucket_table - .get(&EmptyKey, &bucket_id) - .await? - .filter(|b| !b.state.is_deleted()) - .ok_or(Error::NoSuchBucket)?; + .bucket_helper() + .get_existing_bucket(bucket_id) + .await?; let allowed = match endpoint.authorization_type() { Authorization::Read => api_key.allow_read(&bucket_id), @@ -164,9 +164,7 @@ impl ApiHandler for S3ApiServer { }; if !allowed { - return Err(Error::Forbidden( - "Operation is not allowed for this key.".to_string(), - )); + return Err(Error::forbidden("Operation is not allowed for this key.")); } // Look up what CORS rule might apply to response. @@ -309,7 +307,7 @@ impl ApiHandler for S3ApiServer { ) .await } else { - Err(Error::BadRequest(format!( + Err(Error::bad_request(format!( "Invalid endpoint: list-type={}", list_type ))) diff --git a/src/api/s3/bucket.rs b/src/api/s3/bucket.rs index 93048a8c..2071fe55 100644 --- a/src/api/s3/bucket.rs +++ b/src/api/s3/bucket.rs @@ -8,13 +8,13 @@ use garage_model::bucket_table::Bucket; use garage_model::garage::Garage; use garage_model::key_table::Key; use garage_model::permission::BucketKeyPerm; -use garage_model::s3::object_table::ObjectFilter; use garage_table::util::*; use garage_util::crdt::*; use garage_util::data::*; use garage_util::time::*; -use crate::error::*; +use crate::common_error::CommonError; +use crate::s3::error::*; use crate::s3::xml as s3_xml; use crate::signature::verify_signed_content; @@ -130,7 +130,7 @@ pub async fn handle_create_bucket( if let Some(location_constraint) = cmd { if location_constraint != garage.config.s3_api.s3_region { - return Err(Error::BadRequest(format!( + return Err(Error::bad_request(format!( "Cannot satisfy location constraint `{}`: buckets can only be created in region `{}`", location_constraint, garage.config.s3_api.s3_region @@ -158,12 +158,12 @@ pub async fn handle_create_bucket( // otherwise return a forbidden error. let kp = api_key.bucket_permissions(&bucket_id); if !(kp.allow_write || kp.allow_owner) { - return Err(Error::BucketAlreadyExists); + return Err(CommonError::BucketAlreadyExists.into()); } } else { // Create the bucket! if !is_valid_bucket_name(&bucket_name) { - return Err(Error::BadRequest(format!( + return Err(Error::bad_request(format!( "{}: {}", bucket_name, INVALID_BUCKET_NAME_MESSAGE ))); @@ -228,18 +228,8 @@ pub async fn handle_delete_bucket( // Delete bucket // Check bucket is empty - let objects = garage - .object_table - .get_range( - &bucket_id, - None, - Some(ObjectFilter::IsData), - 10, - EnumerationOrder::Forward, - ) - .await?; - if !objects.is_empty() { - return Err(Error::BucketNotEmpty); + if !garage.bucket_helper().is_bucket_empty(bucket_id).await? { + return Err(CommonError::BucketNotEmpty.into()); } // --- done checking, now commit --- diff --git a/src/api/s3/copy.rs b/src/api/s3/copy.rs index 4e94d887..0fc16993 100644 --- a/src/api/s3/copy.rs +++ b/src/api/s3/copy.rs @@ -18,8 +18,8 @@ use garage_model::s3::block_ref_table::*; use garage_model::s3::object_table::*; use garage_model::s3::version_table::*; -use crate::error::*; -use crate::helpers::{parse_bucket_key, resolve_bucket}; +use crate::helpers::parse_bucket_key; +use crate::s3::error::*; use crate::s3::put::{decode_upload_id, get_headers}; use crate::s3::xml::{self as s3_xml, xmlns_tag}; @@ -201,8 +201,8 @@ pub async fn handle_upload_part_copy( let mut ranges = http_range::HttpRange::parse(range_str, source_version_meta.size) .map_err(|e| (e, source_version_meta.size))?; if ranges.len() != 1 { - return Err(Error::BadRequest( - "Invalid x-amz-copy-source-range header: exactly 1 range must be given".into(), + return Err(Error::bad_request( + "Invalid x-amz-copy-source-range header: exactly 1 range must be given", )); } else { ranges.pop().unwrap() @@ -230,8 +230,8 @@ pub async fn handle_upload_part_copy( // This is only for small files, we don't bother handling this. // (in AWS UploadPartCopy works for parts at least 5MB which // is never the case of an inline object) - return Err(Error::BadRequest( - "Source object is too small (minimum part size is 5Mb)".into(), + return Err(Error::bad_request( + "Source object is too small (minimum part size is 5Mb)", )); } ObjectVersionData::FirstBlock(_meta, _first_block_hash) => (), @@ -250,7 +250,7 @@ pub async fn handle_upload_part_copy( // Check this part number hasn't yet been uploaded if let Some(dv) = dest_version { if dv.has_part_number(part_number) { - return Err(Error::BadRequest(format!( + return Err(Error::bad_request(format!( "Part number {} has already been uploaded", part_number ))); @@ -413,10 +413,13 @@ async fn get_copy_source( let copy_source = percent_encoding::percent_decode_str(copy_source).decode_utf8()?; let (source_bucket, source_key) = parse_bucket_key(©_source, None)?; - let source_bucket_id = resolve_bucket(garage, &source_bucket.to_string(), api_key).await?; + let source_bucket_id = garage + .bucket_helper() + .resolve_bucket(&source_bucket.to_string(), api_key) + .await?; if !api_key.allow_read(&source_bucket_id) { - return Err(Error::Forbidden(format!( + return Err(Error::forbidden(format!( "Reading from bucket {} not allowed for this key", source_bucket ))); @@ -536,8 +539,8 @@ impl CopyPreconditionHeaders { (None, None, None, Some(ims)) => v_date > *ims, (None, None, None, None) => true, _ => { - return Err(Error::BadRequest( - "Invalid combination of x-amz-copy-source-if-xxxxx headers".into(), + return Err(Error::bad_request( + "Invalid combination of x-amz-copy-source-if-xxxxx headers", )) } }; diff --git a/src/api/s3/cors.rs b/src/api/s3/cors.rs index 37ea2e43..c7273464 100644 --- a/src/api/s3/cors.rs +++ b/src/api/s3/cors.rs @@ -9,13 +9,12 @@ use hyper::{header::HeaderName, Body, Method, Request, Response, StatusCode}; use serde::{Deserialize, Serialize}; -use crate::error::*; +use crate::s3::error::*; use crate::s3::xml::{to_xml_with_header, xmlns_tag, IntValue, Value}; use crate::signature::verify_signed_content; use garage_model::bucket_table::{Bucket, CorsRule as GarageCorsRule}; use garage_model::garage::Garage; -use garage_table::*; use garage_util::data::*; pub async fn handle_get_cors(bucket: &Bucket) -> Result, Error> { @@ -48,14 +47,11 @@ pub async fn handle_delete_cors( bucket_id: Uuid, ) -> Result, Error> { let mut bucket = garage - .bucket_table - .get(&EmptyKey, &bucket_id) - .await? - .ok_or(Error::NoSuchBucket)?; + .bucket_helper() + .get_existing_bucket(bucket_id) + .await?; - let param = bucket - .params_mut() - .ok_or_internal_error("Bucket should not be deleted at this point")?; + let param = bucket.params_mut().unwrap(); param.cors_config.update(None); garage.bucket_table.insert(&bucket).await?; @@ -78,14 +74,11 @@ pub async fn handle_put_cors( } let mut bucket = garage - .bucket_table - .get(&EmptyKey, &bucket_id) - .await? - .ok_or(Error::NoSuchBucket)?; + .bucket_helper() + .get_existing_bucket(bucket_id) + .await?; - let param = bucket - .params_mut() - .ok_or_internal_error("Bucket should not be deleted at this point")?; + let param = bucket.params_mut().unwrap(); let conf: CorsConfiguration = from_reader(&body as &[u8])?; conf.validate()?; @@ -119,12 +112,7 @@ pub async fn handle_options_s3api( let helper = garage.bucket_helper(); let bucket_id = helper.resolve_global_bucket_name(&bn).await?; if let Some(id) = bucket_id { - let bucket = garage - .bucket_table - .get(&EmptyKey, &id) - .await? - .filter(|b| !b.state.is_deleted()) - .ok_or(Error::NoSuchBucket)?; + let bucket = garage.bucket_helper().get_existing_bucket(id).await?; handle_options_for_bucket(req, &bucket) } else { // If there is a bucket name in the request, but that name @@ -185,7 +173,7 @@ pub fn handle_options_for_bucket( } } - Err(Error::Forbidden("This CORS request is not allowed.".into())) + Err(Error::forbidden("This CORS request is not allowed.")) } pub fn find_matching_cors_rule<'a>( diff --git a/src/api/s3/delete.rs b/src/api/s3/delete.rs index 1e3f1249..5065b285 100644 --- a/src/api/s3/delete.rs +++ b/src/api/s3/delete.rs @@ -8,7 +8,7 @@ use garage_util::time::*; use garage_model::garage::Garage; use garage_model::s3::object_table::*; -use crate::error::*; +use crate::s3::error::*; use crate::s3::xml as s3_xml; use crate::signature::verify_signed_content; diff --git a/src/api/error.rs b/src/api/s3/error.rs similarity index 50% rename from src/api/error.rs rename to src/api/s3/error.rs index 4b7254d2..ac632540 100644 --- a/src/api/error.rs +++ b/src/api/s3/error.rs @@ -2,34 +2,24 @@ use std::convert::TryInto; use err_derive::Error; use hyper::header::HeaderValue; -use hyper::{HeaderMap, StatusCode}; +use hyper::{Body, HeaderMap, StatusCode}; use garage_model::helper::error::Error as HelperError; -use garage_util::error::Error as GarageError; +use crate::common_error::CommonError; +pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInternalError}; +use crate::generic_server::ApiError; use crate::s3::xml as s3_xml; +use crate::signature::error::Error as SignatureError; /// Errors of this crate #[derive(Debug, Error)] pub enum Error { - // Category: internal error - /// Error related to deeper parts of Garage - #[error(display = "Internal error: {}", _0)] - InternalError(#[error(source)] GarageError), - - /// Error related to Hyper - #[error(display = "Internal error (Hyper error): {}", _0)] - Hyper(#[error(source)] hyper::Error), - - /// Error related to HTTP - #[error(display = "Internal error (HTTP error): {}", _0)] - Http(#[error(source)] http::Error), + #[error(display = "{}", _0)] + /// Error from common error + Common(CommonError), // Category: cannot process - /// No proper api key was used, or the signature was invalid - #[error(display = "Forbidden: {}", _0)] - Forbidden(String), - /// Authorization Header Malformed #[error(display = "Authorization header malformed, expected scope: {}", _0)] AuthorizationHeaderMalformed(String), @@ -38,22 +28,10 @@ pub enum Error { #[error(display = "Key not found")] NoSuchKey, - /// The bucket requested don't exists - #[error(display = "Bucket not found")] - NoSuchBucket, - /// The multipart upload requested don't exists #[error(display = "Upload not found")] NoSuchUpload, - /// Tried to create a bucket that already exist - #[error(display = "Bucket already exists")] - BucketAlreadyExists, - - /// Tried to delete a non-empty bucket - #[error(display = "Tried to delete a non-empty bucket")] - BucketNotEmpty, - /// Precondition failed (e.g. x-amz-copy-source-if-match) #[error(display = "At least one of the preconditions you specified did not hold")] PreconditionFailed, @@ -80,10 +58,6 @@ pub enum Error { #[error(display = "Invalid UTF-8: {}", _0)] InvalidUtf8String(#[error(source)] std::string::FromUtf8Error), - /// Some base64 encoded data was badly encoded - #[error(display = "Invalid base64: {}", _0)] - InvalidBase64(#[error(source)] base64::DecodeError), - /// The client sent invalid XML data #[error(display = "Invalid XML: {}", _0)] InvalidXml(String), @@ -96,19 +70,34 @@ pub enum Error { #[error(display = "Invalid HTTP range: {:?}", _0)] InvalidRange(#[error(from)] (http_range::HttpRangeParseError, u64)), - /// The client sent an invalid request - #[error(display = "Bad request: {}", _0)] - BadRequest(String), - - /// The client asked for an invalid return format (invalid Accept header) - #[error(display = "Not acceptable: {}", _0)] - NotAcceptable(String), - /// The client sent a request for an action not supported by garage #[error(display = "Unimplemented action: {}", _0)] NotImplemented(String), } +impl From for Error +where + CommonError: From, +{ + fn from(err: T) -> Self { + Error::Common(CommonError::from(err)) + } +} + +impl CommonErrorDerivative for Error {} + +impl From for Error { + fn from(err: HelperError) -> Self { + match err { + HelperError::Internal(i) => Self::Common(CommonError::InternalError(i)), + HelperError::BadRequest(b) => Self::Common(CommonError::BadRequest(b)), + HelperError::InvalidBucketName(n) => Self::Common(CommonError::InvalidBucketName(n)), + HelperError::NoSuchBucket(n) => Self::Common(CommonError::NoSuchBucket(n)), + e => Self::bad_request(format!("{}", e)), + } + } +} + impl From for Error { fn from(err: roxmltree::Error) -> Self { Self::InvalidXml(format!("{}", err)) @@ -121,88 +110,67 @@ impl From for Error { } } -impl From for Error { - fn from(err: HelperError) -> Self { +impl From for Error { + fn from(err: SignatureError) -> Self { match err { - HelperError::Internal(i) => Self::InternalError(i), - HelperError::BadRequest(b) => Self::BadRequest(b), + SignatureError::Common(c) => Self::Common(c), + SignatureError::AuthorizationHeaderMalformed(c) => { + Self::AuthorizationHeaderMalformed(c) + } + SignatureError::InvalidUtf8Str(i) => Self::InvalidUtf8Str(i), + SignatureError::InvalidHeader(h) => Self::InvalidHeader(h), } } } impl From for Error { fn from(err: multer::Error) -> Self { - Self::BadRequest(err.to_string()) + Self::bad_request(err) } } impl Error { - /// Get the HTTP status code that best represents the meaning of the error for the client - pub fn http_status_code(&self) -> StatusCode { - match self { - Error::NoSuchKey | Error::NoSuchBucket | Error::NoSuchUpload => StatusCode::NOT_FOUND, - Error::BucketNotEmpty | Error::BucketAlreadyExists => StatusCode::CONFLICT, - Error::PreconditionFailed => StatusCode::PRECONDITION_FAILED, - Error::Forbidden(_) => StatusCode::FORBIDDEN, - Error::NotAcceptable(_) => StatusCode::NOT_ACCEPTABLE, - Error::InternalError( - GarageError::Timeout - | GarageError::RemoteError(_) - | GarageError::Quorum(_, _, _, _), - ) => StatusCode::SERVICE_UNAVAILABLE, - Error::InternalError(_) | Error::Hyper(_) | Error::Http(_) => { - StatusCode::INTERNAL_SERVER_ERROR - } - Error::InvalidRange(_) => StatusCode::RANGE_NOT_SATISFIABLE, - Error::NotImplemented(_) => StatusCode::NOT_IMPLEMENTED, - _ => StatusCode::BAD_REQUEST, - } - } - pub fn aws_code(&self) -> &'static str { match self { + Error::Common(c) => c.aws_code(), Error::NoSuchKey => "NoSuchKey", - Error::NoSuchBucket => "NoSuchBucket", Error::NoSuchUpload => "NoSuchUpload", - Error::BucketAlreadyExists => "BucketAlreadyExists", - Error::BucketNotEmpty => "BucketNotEmpty", Error::PreconditionFailed => "PreconditionFailed", Error::InvalidPart => "InvalidPart", Error::InvalidPartOrder => "InvalidPartOrder", Error::EntityTooSmall => "EntityTooSmall", - Error::Forbidden(_) => "AccessDenied", Error::AuthorizationHeaderMalformed(_) => "AuthorizationHeaderMalformed", Error::NotImplemented(_) => "NotImplemented", - Error::InternalError( - GarageError::Timeout - | GarageError::RemoteError(_) - | GarageError::Quorum(_, _, _, _), - ) => "ServiceUnavailable", - Error::InternalError(_) | Error::Hyper(_) | Error::Http(_) => "InternalError", - _ => "InvalidRequest", + Error::InvalidXml(_) => "MalformedXML", + Error::InvalidRange(_) => "InvalidRange", + Error::InvalidUtf8Str(_) | Error::InvalidUtf8String(_) | Error::InvalidHeader(_) => { + "InvalidRequest" + } + } + } +} + +impl ApiError for Error { + /// Get the HTTP status code that best represents the meaning of the error for the client + fn http_status_code(&self) -> StatusCode { + match self { + Error::Common(c) => c.http_status_code(), + Error::NoSuchKey | Error::NoSuchUpload => StatusCode::NOT_FOUND, + Error::PreconditionFailed => StatusCode::PRECONDITION_FAILED, + Error::InvalidRange(_) => StatusCode::RANGE_NOT_SATISFIABLE, + Error::NotImplemented(_) => StatusCode::NOT_IMPLEMENTED, + Error::AuthorizationHeaderMalformed(_) + | Error::InvalidPart + | Error::InvalidPartOrder + | Error::EntityTooSmall + | Error::InvalidXml(_) + | Error::InvalidUtf8Str(_) + | Error::InvalidUtf8String(_) + | Error::InvalidHeader(_) => StatusCode::BAD_REQUEST, } } - pub fn aws_xml(&self, garage_region: &str, path: &str) -> String { - let error = s3_xml::Error { - code: s3_xml::Value(self.aws_code().to_string()), - message: s3_xml::Value(format!("{}", self)), - resource: Some(s3_xml::Value(path.to_string())), - region: Some(s3_xml::Value(garage_region.to_string())), - }; - s3_xml::to_xml_with_header(&error).unwrap_or_else(|_| { - r#" - - - InternalError - XML encoding of error failed - - "# - .into() - }) - } - - pub fn add_headers(&self, header_map: &mut HeaderMap) { + fn add_http_headers(&self, header_map: &mut HeaderMap) { use hyper::header; #[allow(clippy::single_match)] match self { @@ -217,68 +185,23 @@ impl Error { _ => (), } } -} -/// Trait to map error to the Bad Request error code -pub trait OkOrBadRequest { - type S; - fn ok_or_bad_request>(self, reason: M) -> Result; -} - -impl OkOrBadRequest for Result -where - E: std::fmt::Display, -{ - type S = T; - fn ok_or_bad_request>(self, reason: M) -> Result { - match self { - Ok(x) => Ok(x), - Err(e) => Err(Error::BadRequest(format!("{}: {}", reason.as_ref(), e))), - } - } -} - -impl OkOrBadRequest for Option { - type S = T; - fn ok_or_bad_request>(self, reason: M) -> Result { - match self { - Some(x) => Ok(x), - None => Err(Error::BadRequest(reason.as_ref().to_string())), - } - } -} - -/// Trait to map an error to an Internal Error code -pub trait OkOrInternalError { - type S; - fn ok_or_internal_error>(self, reason: M) -> Result; -} - -impl OkOrInternalError for Result -where - E: std::fmt::Display, -{ - type S = T; - fn ok_or_internal_error>(self, reason: M) -> Result { - match self { - Ok(x) => Ok(x), - Err(e) => Err(Error::InternalError(GarageError::Message(format!( - "{}: {}", - reason.as_ref(), - e - )))), - } - } -} - -impl OkOrInternalError for Option { - type S = T; - fn ok_or_internal_error>(self, reason: M) -> Result { - match self { - Some(x) => Ok(x), - None => Err(Error::InternalError(GarageError::Message( - reason.as_ref().to_string(), - ))), - } + fn http_body(&self, garage_region: &str, path: &str) -> Body { + let error = s3_xml::Error { + code: s3_xml::Value(self.aws_code().to_string()), + message: s3_xml::Value(format!("{}", self)), + resource: Some(s3_xml::Value(path.to_string())), + region: Some(s3_xml::Value(garage_region.to_string())), + }; + Body::from(s3_xml::to_xml_with_header(&error).unwrap_or_else(|_| { + r#" + + + InternalError + XML encoding of error failed + + "# + .into() + })) } } diff --git a/src/api/s3/get.rs b/src/api/s3/get.rs index 3edf22a6..7fa1a177 100644 --- a/src/api/s3/get.rs +++ b/src/api/s3/get.rs @@ -17,7 +17,7 @@ use garage_model::garage::Garage; use garage_model::s3::object_table::*; use garage_model::s3::version_table::*; -use crate::error::*; +use crate::s3::error::*; const X_AMZ_MP_PARTS_COUNT: &str = "x-amz-mp-parts-count"; @@ -210,8 +210,8 @@ pub async fn handle_get( match (part_number, parse_range_header(req, last_v_meta.size)?) { (Some(_), Some(_)) => { - return Err(Error::BadRequest( - "Cannot specify both partNumber and Range header".into(), + return Err(Error::bad_request( + "Cannot specify both partNumber and Range header", )); } (Some(pn), None) => { @@ -302,9 +302,9 @@ async fn handle_get_range( let body: Body = Body::from(bytes[begin as usize..end as usize].to_vec()); Ok(resp_builder.body(body)?) } else { - None.ok_or_internal_error( + Err(Error::internal_error( "Requested range not present in inline bytes when it should have been", - ) + )) } } ObjectVersionData::FirstBlock(_meta, _first_block_hash) => { diff --git a/src/api/s3/list.rs b/src/api/s3/list.rs index e2848c57..e5f486c8 100644 --- a/src/api/s3/list.rs +++ b/src/api/s3/list.rs @@ -16,8 +16,8 @@ use garage_model::s3::version_table::Version; use garage_table::{EmptyKey, EnumerationOrder}; use crate::encoding::*; -use crate::error::*; use crate::helpers::key_after_prefix; +use crate::s3::error::*; use crate::s3::put as s3_put; use crate::s3::xml as s3_xml; @@ -582,13 +582,19 @@ impl ListObjectsQuery { // representing the key to start with. (Some(token), _) => match &token[..1] { "[" => Ok(RangeBegin::IncludingKey { - key: String::from_utf8(base64::decode(token[1..].as_bytes())?)?, + key: String::from_utf8( + base64::decode(token[1..].as_bytes()) + .ok_or_bad_request("Invalid continuation token")?, + )?, fallback_key: None, }), "]" => Ok(RangeBegin::AfterKey { - key: String::from_utf8(base64::decode(token[1..].as_bytes())?)?, + key: String::from_utf8( + base64::decode(token[1..].as_bytes()) + .ok_or_bad_request("Invalid continuation token")?, + )?, }), - _ => Err(Error::BadRequest("Invalid continuation token".to_string())), + _ => Err(Error::bad_request("Invalid continuation token")), }, // StartAfter has defined semantics in the spec: diff --git a/src/api/s3/mod.rs b/src/api/s3/mod.rs index 3f5c1915..7b56d4d8 100644 --- a/src/api/s3/mod.rs +++ b/src/api/s3/mod.rs @@ -1,4 +1,5 @@ pub mod api_server; +pub mod error; mod bucket; mod copy; diff --git a/src/api/s3/post_object.rs b/src/api/s3/post_object.rs index 86fa7880..dc640f43 100644 --- a/src/api/s3/post_object.rs +++ b/src/api/s3/post_object.rs @@ -14,8 +14,7 @@ use serde::Deserialize; use garage_model::garage::Garage; -use crate::error::*; -use crate::helpers::resolve_bucket; +use crate::s3::error::*; use crate::s3::put::{get_headers, save_stream}; use crate::s3::xml as s3_xml; use crate::signature::payload::{parse_date, verify_v4}; @@ -48,9 +47,7 @@ pub async fn handle_post_object( let field = if let Some(field) = multipart.next_field().await? { field } else { - return Err(Error::BadRequest( - "Request did not contain a file".to_owned(), - )); + return Err(Error::bad_request("Request did not contain a file")); }; let name: HeaderName = if let Some(Ok(name)) = field.name().map(TryInto::try_into) { name @@ -66,14 +63,14 @@ pub async fn handle_post_object( "tag" => (/* tag need to be reencoded, but we don't support them yet anyway */), "acl" => { if params.insert("x-amz-acl", content).is_some() { - return Err(Error::BadRequest( - "Field 'acl' provided more than one time".to_string(), + return Err(Error::bad_request( + "Field 'acl' provided more than one time", )); } } _ => { if params.insert(&name, content).is_some() { - return Err(Error::BadRequest(format!( + return Err(Error::bad_request(format!( "Field '{}' provided more than one time", name ))); @@ -90,9 +87,7 @@ pub async fn handle_post_object( .to_str()?; let credential = params .get("x-amz-credential") - .ok_or_else(|| { - Error::Forbidden("Garage does not support anonymous access yet".to_string()) - })? + .ok_or_else(|| Error::forbidden("Garage does not support anonymous access yet"))? .to_str()?; let policy = params .get("policy") @@ -129,15 +124,16 @@ pub async fn handle_post_object( ) .await?; - let bucket_id = resolve_bucket(&garage, &bucket, &api_key).await?; + let bucket_id = garage + .bucket_helper() + .resolve_bucket(&bucket, &api_key) + .await?; if !api_key.allow_write(&bucket_id) { - return Err(Error::Forbidden( - "Operation is not allowed for this key.".to_string(), - )); + return Err(Error::forbidden("Operation is not allowed for this key.")); } - let decoded_policy = base64::decode(&policy)?; + let decoded_policy = base64::decode(&policy).ok_or_bad_request("Invalid policy")?; let decoded_policy: Policy = serde_json::from_slice(&decoded_policy).ok_or_bad_request("Invalid policy")?; @@ -145,9 +141,7 @@ pub async fn handle_post_object( .ok_or_bad_request("Invalid expiration date")? .into(); if Utc::now() - expiration > Duration::zero() { - return Err(Error::BadRequest( - "Expiration date is in the paste".to_string(), - )); + return Err(Error::bad_request("Expiration date is in the paste")); } let mut conditions = decoded_policy.into_conditions()?; @@ -159,7 +153,7 @@ pub async fn handle_post_object( "policy" | "x-amz-signature" => (), // this is always accepted, as it's required to validate other fields "content-type" => { let conds = conditions.params.remove("content-type").ok_or_else(|| { - Error::BadRequest(format!("Key '{}' is not allowed in policy", param_key)) + Error::bad_request(format!("Key '{}' is not allowed in policy", param_key)) })?; for cond in conds { let ok = match cond { @@ -169,7 +163,7 @@ pub async fn handle_post_object( } }; if !ok { - return Err(Error::BadRequest(format!( + return Err(Error::bad_request(format!( "Key '{}' has value not allowed in policy", param_key ))); @@ -178,7 +172,7 @@ pub async fn handle_post_object( } "key" => { let conds = conditions.params.remove("key").ok_or_else(|| { - Error::BadRequest(format!("Key '{}' is not allowed in policy", param_key)) + Error::bad_request(format!("Key '{}' is not allowed in policy", param_key)) })?; for cond in conds { let ok = match cond { @@ -186,7 +180,7 @@ pub async fn handle_post_object( Operation::StartsWith(s) => key.starts_with(&s), }; if !ok { - return Err(Error::BadRequest(format!( + return Err(Error::bad_request(format!( "Key '{}' has value not allowed in policy", param_key ))); @@ -201,7 +195,7 @@ pub async fn handle_post_object( continue; } let conds = conditions.params.remove(¶m_key).ok_or_else(|| { - Error::BadRequest(format!("Key '{}' is not allowed in policy", param_key)) + Error::bad_request(format!("Key '{}' is not allowed in policy", param_key)) })?; for cond in conds { let ok = match cond { @@ -209,7 +203,7 @@ pub async fn handle_post_object( Operation::StartsWith(s) => value.to_str()?.starts_with(s.as_str()), }; if !ok { - return Err(Error::BadRequest(format!( + return Err(Error::bad_request(format!( "Key '{}' has value not allowed in policy", param_key ))); @@ -220,7 +214,7 @@ pub async fn handle_post_object( } if let Some((param_key, _)) = conditions.params.iter().next() { - return Err(Error::BadRequest(format!( + return Err(Error::bad_request(format!( "Key '{}' is required in policy, but no value was provided", param_key ))); @@ -326,7 +320,7 @@ impl Policy { match condition { PolicyCondition::Equal(map) => { if map.len() != 1 { - return Err(Error::BadRequest("Invalid policy item".to_owned())); + return Err(Error::bad_request("Invalid policy item")); } let (mut k, v) = map.into_iter().next().expect("size was verified"); k.make_ascii_lowercase(); @@ -334,7 +328,7 @@ impl Policy { } PolicyCondition::OtherOp([cond, mut key, value]) => { if key.remove(0) != '$' { - return Err(Error::BadRequest("Invalid policy item".to_owned())); + return Err(Error::bad_request("Invalid policy item")); } key.make_ascii_lowercase(); match cond.as_str() { @@ -347,7 +341,7 @@ impl Policy { .or_default() .push(Operation::StartsWith(value)); } - _ => return Err(Error::BadRequest("Invalid policy item".to_owned())), + _ => return Err(Error::bad_request("Invalid policy item")), } } PolicyCondition::SizeRange(key, min, max) => { @@ -355,7 +349,7 @@ impl Policy { length.0 = length.0.max(min); length.1 = length.1.min(max); } else { - return Err(Error::BadRequest("Invalid policy item".to_owned())); + return Err(Error::bad_request("Invalid policy item")); } } } @@ -420,15 +414,15 @@ where self.read += bytes.len() as u64; // optimization to fail early when we know before the end it's too long if self.length.end() < &self.read { - return Poll::Ready(Some(Err(Error::BadRequest( - "File size does not match policy".to_owned(), + return Poll::Ready(Some(Err(Error::bad_request( + "File size does not match policy", )))); } } Poll::Ready(None) => { if !self.length.contains(&self.read) { - return Poll::Ready(Some(Err(Error::BadRequest( - "File size does not match policy".to_owned(), + return Poll::Ready(Some(Err(Error::bad_request( + "File size does not match policy", )))); } } diff --git a/src/api/s3/put.rs b/src/api/s3/put.rs index 89aa8d84..8b06ef3f 100644 --- a/src/api/s3/put.rs +++ b/src/api/s3/put.rs @@ -19,7 +19,7 @@ use garage_model::s3::block_ref_table::*; use garage_model::s3::object_table::*; use garage_model::s3::version_table::*; -use crate::error::*; +use crate::s3::error::*; use crate::s3::xml as s3_xml; use crate::signature::verify_signed_content; @@ -183,8 +183,8 @@ fn ensure_checksum_matches( ) -> Result<(), Error> { if let Some(expected_sha256) = content_sha256 { if expected_sha256 != data_sha256sum { - return Err(Error::BadRequest( - "Unable to validate x-amz-content-sha256".to_string(), + return Err(Error::bad_request( + "Unable to validate x-amz-content-sha256", )); } else { trace!("Successfully validated x-amz-content-sha256"); @@ -192,9 +192,7 @@ fn ensure_checksum_matches( } if let Some(expected_md5) = content_md5 { if expected_md5.trim_matches('"') != base64::encode(data_md5sum) { - return Err(Error::BadRequest( - "Unable to validate content-md5".to_string(), - )); + return Err(Error::bad_request("Unable to validate content-md5")); } else { trace!("Successfully validated content-md5"); } @@ -428,7 +426,7 @@ pub async fn handle_put_part( // Check part hasn't already been uploaded if let Some(v) = version { if v.has_part_number(part_number) { - return Err(Error::BadRequest(format!( + return Err(Error::bad_request(format!( "Part number {} has already been uploaded", part_number ))); @@ -513,7 +511,7 @@ pub async fn handle_complete_multipart_upload( let version = version.ok_or(Error::NoSuchKey)?; if version.blocks.is_empty() { - return Err(Error::BadRequest("No data was uploaded".to_string())); + return Err(Error::bad_request("No data was uploaded")); } let headers = match object_version.state { @@ -574,8 +572,8 @@ pub async fn handle_complete_multipart_upload( .map(|x| x.part_number) .eq(block_parts.into_iter()); if !same_parts { - return Err(Error::BadRequest( - "Part numbers in block list and part list do not match. This can happen if a part was partially uploaded. Please abort the multipart upload and try again.".into(), + return Err(Error::bad_request( + "Part numbers in block list and part list do not match. This can happen if a part was partially uploaded. Please abort the multipart upload and try again." )); } diff --git a/src/api/s3/router.rs b/src/api/s3/router.rs index 0525c649..44f581ff 100644 --- a/src/api/s3/router.rs +++ b/src/api/s3/router.rs @@ -1,5 +1,3 @@ -use crate::error::{Error, OkOrBadRequest}; - use std::borrow::Cow; use hyper::header::HeaderValue; @@ -7,6 +5,7 @@ use hyper::{HeaderMap, Method, Request}; use crate::helpers::Authorization; use crate::router_macros::{generateQueryParameters, router_match}; +use crate::s3::error::*; router_match! {@func @@ -343,7 +342,7 @@ impl Endpoint { Method::POST => Self::from_post(key, &mut query)?, Method::PUT => Self::from_put(key, &mut query, req.headers())?, Method::DELETE => Self::from_delete(key, &mut query)?, - _ => return Err(Error::BadRequest("Unknown method".to_owned())), + _ => return Err(Error::bad_request("Unknown method")), }; if let Some(message) = query.nonempty_message() { diff --git a/src/api/s3/website.rs b/src/api/s3/website.rs index 561130dc..77738971 100644 --- a/src/api/s3/website.rs +++ b/src/api/s3/website.rs @@ -4,13 +4,12 @@ use std::sync::Arc; use hyper::{Body, Request, Response, StatusCode}; use serde::{Deserialize, Serialize}; -use crate::error::*; +use crate::s3::error::*; use crate::s3::xml::{to_xml_with_header, xmlns_tag, IntValue, Value}; use crate::signature::verify_signed_content; use garage_model::bucket_table::*; use garage_model::garage::Garage; -use garage_table::*; use garage_util::data::*; pub async fn handle_get_website(bucket: &Bucket) -> Result, Error> { @@ -47,14 +46,11 @@ pub async fn handle_delete_website( bucket_id: Uuid, ) -> Result, Error> { let mut bucket = garage - .bucket_table - .get(&EmptyKey, &bucket_id) - .await? - .ok_or(Error::NoSuchBucket)?; + .bucket_helper() + .get_existing_bucket(bucket_id) + .await?; - let param = bucket - .params_mut() - .ok_or_internal_error("Bucket should not be deleted at this point")?; + let param = bucket.params_mut().unwrap(); param.website_config.update(None); garage.bucket_table.insert(&bucket).await?; @@ -77,14 +73,11 @@ pub async fn handle_put_website( } let mut bucket = garage - .bucket_table - .get(&EmptyKey, &bucket_id) - .await? - .ok_or(Error::NoSuchBucket)?; + .bucket_helper() + .get_existing_bucket(bucket_id) + .await?; - let param = bucket - .params_mut() - .ok_or_internal_error("Bucket should not be deleted at this point")?; + let param = bucket.params_mut().unwrap(); let conf: WebsiteConfiguration = from_reader(&body as &[u8])?; conf.validate()?; @@ -176,8 +169,8 @@ impl WebsiteConfiguration { || self.index_document.is_some() || self.routing_rules.is_some()) { - return Err(Error::BadRequest( - "Bad XML: can't have RedirectAllRequestsTo and other fields".to_owned(), + return Err(Error::bad_request( + "Bad XML: can't have RedirectAllRequestsTo and other fields", )); } if let Some(ref ed) = self.error_document { @@ -222,8 +215,8 @@ impl WebsiteConfiguration { impl Key { pub fn validate(&self) -> Result<(), Error> { if self.key.0.is_empty() { - Err(Error::BadRequest( - "Bad XML: error document specified but empty".to_owned(), + Err(Error::bad_request( + "Bad XML: error document specified but empty", )) } else { Ok(()) @@ -234,8 +227,8 @@ impl Key { impl Suffix { pub fn validate(&self) -> Result<(), Error> { if self.suffix.0.is_empty() | self.suffix.0.contains('/') { - Err(Error::BadRequest( - "Bad XML: index document is empty or contains /".to_owned(), + Err(Error::bad_request( + "Bad XML: index document is empty or contains /", )) } else { Ok(()) @@ -247,7 +240,7 @@ impl Target { pub fn validate(&self) -> Result<(), Error> { if let Some(ref protocol) = self.protocol { if protocol.0 != "http" && protocol.0 != "https" { - return Err(Error::BadRequest("Bad XML: invalid protocol".to_owned())); + return Err(Error::bad_request("Bad XML: invalid protocol")); } } Ok(()) @@ -269,19 +262,19 @@ impl Redirect { pub fn validate(&self, has_prefix: bool) -> Result<(), Error> { if self.replace_prefix.is_some() { if self.replace_full.is_some() { - return Err(Error::BadRequest( - "Bad XML: both ReplaceKeyPrefixWith and ReplaceKeyWith are set".to_owned(), + return Err(Error::bad_request( + "Bad XML: both ReplaceKeyPrefixWith and ReplaceKeyWith are set", )); } if !has_prefix { - return Err(Error::BadRequest( - "Bad XML: ReplaceKeyPrefixWith is set, but KeyPrefixEquals isn't".to_owned(), + return Err(Error::bad_request( + "Bad XML: ReplaceKeyPrefixWith is set, but KeyPrefixEquals isn't", )); } } if let Some(ref protocol) = self.protocol { if protocol.0 != "http" && protocol.0 != "https" { - return Err(Error::BadRequest("Bad XML: invalid protocol".to_owned())); + return Err(Error::bad_request("Bad XML: invalid protocol")); } } // TODO there are probably more invalide cases, but which ones? diff --git a/src/api/s3/xml.rs b/src/api/s3/xml.rs index 75ec4559..111657a0 100644 --- a/src/api/s3/xml.rs +++ b/src/api/s3/xml.rs @@ -1,7 +1,7 @@ use quick_xml::se::to_string; use serde::{Deserialize, Serialize, Serializer}; -use crate::Error as ApiError; +use crate::s3::error::Error as ApiError; pub fn to_xml_with_header(x: &T) -> Result { let mut xml = r#""#.to_string(); diff --git a/src/api/signature/error.rs b/src/api/signature/error.rs new file mode 100644 index 00000000..f5a067bd --- /dev/null +++ b/src/api/signature/error.rs @@ -0,0 +1,36 @@ +use err_derive::Error; + +use crate::common_error::CommonError; +pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInternalError}; + +/// Errors of this crate +#[derive(Debug, Error)] +pub enum Error { + #[error(display = "{}", _0)] + /// Error from common error + Common(CommonError), + + /// Authorization Header Malformed + #[error(display = "Authorization header malformed, expected scope: {}", _0)] + AuthorizationHeaderMalformed(String), + + // Category: bad request + /// The request contained an invalid UTF-8 sequence in its path or in other parameters + #[error(display = "Invalid UTF-8: {}", _0)] + InvalidUtf8Str(#[error(source)] std::str::Utf8Error), + + /// The client sent a header with invalid value + #[error(display = "Invalid header value: {}", _0)] + InvalidHeader(#[error(source)] hyper::header::ToStrError), +} + +impl From for Error +where + CommonError: From, +{ + fn from(err: T) -> Self { + Error::Common(CommonError::from(err)) + } +} + +impl CommonErrorDerivative for Error {} diff --git a/src/api/signature/mod.rs b/src/api/signature/mod.rs index 5646f4fa..dd5b590c 100644 --- a/src/api/signature/mod.rs +++ b/src/api/signature/mod.rs @@ -4,11 +4,12 @@ use sha2::Sha256; use garage_util::data::{sha256sum, Hash}; -use crate::error::*; - +pub mod error; pub mod payload; pub mod streaming; +use error::*; + pub const SHORT_DATE: &str = "%Y%m%d"; pub const LONG_DATETIME: &str = "%Y%m%dT%H%M%SZ"; @@ -16,7 +17,7 @@ type HmacSha256 = Hmac; pub fn verify_signed_content(expected_sha256: Hash, body: &[u8]) -> Result<(), Error> { if expected_sha256 != sha256sum(body) { - return Err(Error::BadRequest( + return Err(Error::bad_request( "Request content hash does not match signed hash".to_string(), )); } diff --git a/src/api/signature/payload.rs b/src/api/signature/payload.rs index 9137dd2d..4c7934e5 100644 --- a/src/api/signature/payload.rs +++ b/src/api/signature/payload.rs @@ -15,7 +15,7 @@ use super::LONG_DATETIME; use super::{compute_scope, signing_hmac}; use crate::encoding::uri_encode; -use crate::error::*; +use crate::signature::error::*; pub async fn check_payload_signature( garage: &Garage, @@ -105,7 +105,7 @@ fn parse_authorization( let (auth_kind, rest) = authorization.split_at(first_space); if auth_kind != "AWS4-HMAC-SHA256" { - return Err(Error::BadRequest("Unsupported authorization method".into())); + return Err(Error::bad_request("Unsupported authorization method")); } let mut auth_params = HashMap::new(); @@ -129,10 +129,11 @@ fn parse_authorization( let date = headers .get("x-amz-date") .ok_or_bad_request("Missing X-Amz-Date field") + .map_err(Error::from) .and_then(|d| parse_date(d))?; if Utc::now() - date > Duration::hours(24) { - return Err(Error::BadRequest("Date is too old".to_string())); + return Err(Error::bad_request("Date is too old".to_string())); } let auth = Authorization { @@ -156,7 +157,7 @@ fn parse_query_authorization( headers: &HashMap, ) -> Result { if algorithm != "AWS4-HMAC-SHA256" { - return Err(Error::BadRequest( + return Err(Error::bad_request( "Unsupported authorization method".to_string(), )); } @@ -179,10 +180,10 @@ fn parse_query_authorization( .get("x-amz-expires") .ok_or_bad_request("X-Amz-Expires not found in query parameters")? .parse() - .map_err(|_| Error::BadRequest("X-Amz-Expires is not a number".to_string()))?; + .map_err(|_| Error::bad_request("X-Amz-Expires is not a number".to_string()))?; if duration > 7 * 24 * 3600 { - return Err(Error::BadRequest( + return Err(Error::bad_request( "X-Amz-Exprires may not exceed a week".to_string(), )); } @@ -190,10 +191,11 @@ fn parse_query_authorization( let date = headers .get("x-amz-date") .ok_or_bad_request("Missing X-Amz-Date field") + .map_err(Error::from) .and_then(|d| parse_date(d))?; if Utc::now() - date > Duration::seconds(duration) { - return Err(Error::BadRequest("Date is too old".to_string())); + return Err(Error::bad_request("Date is too old".to_string())); } Ok(Authorization { @@ -301,7 +303,7 @@ pub async fn verify_v4( .get(&EmptyKey, &key_id) .await? .filter(|k| !k.state.is_deleted()) - .ok_or_else(|| Error::Forbidden(format!("No such key: {}", &key_id)))?; + .ok_or_else(|| Error::forbidden(format!("No such key: {}", &key_id)))?; let key_p = key.params().unwrap(); let mut hmac = signing_hmac( @@ -314,7 +316,7 @@ pub async fn verify_v4( hmac.update(payload); let our_signature = hex::encode(hmac.finalize().into_bytes()); if signature != our_signature { - return Err(Error::Forbidden("Invalid signature".to_string())); + return Err(Error::forbidden("Invalid signature".to_string())); } Ok(key) diff --git a/src/api/signature/streaming.rs b/src/api/signature/streaming.rs index ded9d993..c8358c4f 100644 --- a/src/api/signature/streaming.rs +++ b/src/api/signature/streaming.rs @@ -12,7 +12,7 @@ use garage_util::data::Hash; use super::{compute_scope, sha256sum, HmacSha256, LONG_DATETIME}; -use crate::error::*; +use crate::signature::error::*; pub fn parse_streaming_body( api_key: &Key, @@ -87,7 +87,7 @@ fn compute_streaming_payload_signature( let mut hmac = signing_hmac.clone(); hmac.update(string_to_sign.as_bytes()); - Hash::try_from(&hmac.finalize().into_bytes()).ok_or_internal_error("Invalid signature") + Ok(Hash::try_from(&hmac.finalize().into_bytes()).ok_or_internal_error("Invalid signature")?) } mod payload { @@ -163,10 +163,10 @@ impl From for Error { match err { SignedPayloadStreamError::Stream(e) => e, SignedPayloadStreamError::InvalidSignature => { - Error::BadRequest("Invalid payload signature".into()) + Error::bad_request("Invalid payload signature") } SignedPayloadStreamError::Message(e) => { - Error::BadRequest(format!("Chunk format error: {}", e)) + Error::bad_request(format!("Chunk format error: {}", e)) } } } diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index 3b69d7bc..902f67f8 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -27,10 +27,8 @@ garage_rpc = { version = "0.7.0", path = "../rpc" } garage_table = { version = "0.7.0", path = "../table" } garage_util = { version = "0.7.0", path = "../util" } garage_web = { version = "0.7.0", path = "../web" } -garage_admin = { version = "0.7.0", path = "../admin" } bytes = "1.0" -git-version = "0.3.4" hex = "0.4" tracing = { version = "0.1.30", features = ["log-always"] } pretty_env_logger = "0.4" @@ -54,6 +52,11 @@ tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi #netapp = { version = "0.4", path = "../../../netapp" } netapp = "0.4" +opentelemetry = { version = "0.17", features = [ "rt-tokio" ] } +opentelemetry-prometheus = "0.10" +opentelemetry-otlp = "0.10" +prometheus = "0.13" + [dev-dependencies] aws-sdk-s3 = "0.8" chrono = "0.4" diff --git a/src/garage/admin.rs b/src/garage/admin.rs index af0c3f22..bc1f494a 100644 --- a/src/garage/admin.rs +++ b/src/garage/admin.rs @@ -22,7 +22,6 @@ use garage_model::helper::error::{Error, OkOrBadRequest}; use garage_model::key_table::*; use garage_model::migrate::Migrate; use garage_model::permission::*; -use garage_model::s3::object_table::ObjectFilter; use crate::cli::*; use crate::repair::Repair; @@ -213,18 +212,7 @@ impl AdminRpcHandler { } // Check bucket is empty - let objects = self - .garage - .object_table - .get_range( - &bucket_id, - None, - Some(ObjectFilter::IsData), - 10, - EnumerationOrder::Forward, - ) - .await?; - if !objects.is_empty() { + if !helper.is_bucket_empty(bucket_id).await? { return Err(Error::BadRequest(format!( "Bucket {} is not empty", query.name @@ -261,6 +249,7 @@ impl AdminRpcHandler { async fn handle_alias_bucket(&self, query: &AliasBucketOpt) -> Result { let helper = self.garage.bucket_helper(); + let key_helper = self.garage.key_helper(); let bucket_id = helper .resolve_global_bucket_name(&query.existing_bucket) @@ -268,7 +257,7 @@ impl AdminRpcHandler { .ok_or_bad_request("Bucket not found")?; if let Some(key_pattern) = &query.local { - let key = helper.get_existing_matching_key(key_pattern).await?; + let key = key_helper.get_existing_matching_key(key_pattern).await?; helper .set_local_bucket_alias(bucket_id, &key.key_id, &query.new_name) @@ -290,9 +279,10 @@ impl AdminRpcHandler { async fn handle_unalias_bucket(&self, query: &UnaliasBucketOpt) -> Result { let helper = self.garage.bucket_helper(); + let key_helper = self.garage.key_helper(); if let Some(key_pattern) = &query.local { - let key = helper.get_existing_matching_key(key_pattern).await?; + let key = key_helper.get_existing_matching_key(key_pattern).await?; let bucket_id = key .state @@ -331,12 +321,15 @@ impl AdminRpcHandler { async fn handle_bucket_allow(&self, query: &PermBucketOpt) -> Result { let helper = self.garage.bucket_helper(); + let key_helper = self.garage.key_helper(); let bucket_id = helper .resolve_global_bucket_name(&query.bucket) .await? .ok_or_bad_request("Bucket not found")?; - let key = helper.get_existing_matching_key(&query.key_pattern).await?; + let key = key_helper + .get_existing_matching_key(&query.key_pattern) + .await?; let allow_read = query.read || key.allow_read(&bucket_id); let allow_write = query.write || key.allow_write(&bucket_id); @@ -363,12 +356,15 @@ impl AdminRpcHandler { async fn handle_bucket_deny(&self, query: &PermBucketOpt) -> Result { let helper = self.garage.bucket_helper(); + let key_helper = self.garage.key_helper(); let bucket_id = helper .resolve_global_bucket_name(&query.bucket) .await? .ok_or_bad_request("Bucket not found")?; - let key = helper.get_existing_matching_key(&query.key_pattern).await?; + let key = key_helper + .get_existing_matching_key(&query.key_pattern) + .await?; let allow_read = !query.read && key.allow_read(&bucket_id); let allow_write = !query.write && key.allow_write(&bucket_id); @@ -469,7 +465,7 @@ impl AdminRpcHandler { async fn handle_key_info(&self, query: &KeyOpt) -> Result { let key = self .garage - .bucket_helper() + .key_helper() .get_existing_matching_key(&query.key_pattern) .await?; self.key_info_result(key).await @@ -484,7 +480,7 @@ impl AdminRpcHandler { async fn handle_rename_key(&self, query: &KeyRenameOpt) -> Result { let mut key = self .garage - .bucket_helper() + .key_helper() .get_existing_matching_key(&query.key_pattern) .await?; key.params_mut() @@ -496,9 +492,11 @@ impl AdminRpcHandler { } async fn handle_delete_key(&self, query: &KeyDeleteOpt) -> Result { - let helper = self.garage.bucket_helper(); + let key_helper = self.garage.key_helper(); - let mut key = helper.get_existing_matching_key(&query.key_pattern).await?; + let mut key = key_helper + .get_existing_matching_key(&query.key_pattern) + .await?; if !query.yes { return Err(Error::BadRequest( @@ -506,32 +504,7 @@ impl AdminRpcHandler { )); } - let state = key.state.as_option_mut().unwrap(); - - // --- done checking, now commit --- - // (the step at unset_local_bucket_alias will fail if a bucket - // does not have another alias, the deletion will be - // interrupted in the middle if that happens) - - // 1. Delete local aliases - for (alias, _, to) in state.local_aliases.items().iter() { - if let Some(bucket_id) = to { - helper - .unset_local_bucket_alias(*bucket_id, &key.key_id, alias) - .await?; - } - } - - // 2. Remove permissions on all authorized buckets - for (ab_id, _auth) in state.authorized_buckets.items().iter() { - helper - .set_bucket_key_permissions(*ab_id, &key.key_id, BucketKeyPerm::NO_PERMISSIONS) - .await?; - } - - // 3. Actually delete key - key.state = Deletable::delete(); - self.garage.key_table.insert(&key).await?; + key_helper.delete_key(&mut key).await?; Ok(AdminRpc::Ok(format!( "Key {} was deleted successfully.", @@ -542,7 +515,7 @@ impl AdminRpcHandler { async fn handle_allow_key(&self, query: &KeyPermOpt) -> Result { let mut key = self .garage - .bucket_helper() + .key_helper() .get_existing_matching_key(&query.key_pattern) .await?; if query.create_bucket { @@ -555,7 +528,7 @@ impl AdminRpcHandler { async fn handle_deny_key(&self, query: &KeyPermOpt) -> Result { let mut key = self .garage - .bucket_helper() + .key_helper() .get_existing_matching_key(&query.key_pattern) .await?; if query.create_bucket { @@ -696,11 +669,7 @@ impl AdminRpcHandler { writeln!( &mut ret, "\nGarage version: {}", - option_env!("GIT_VERSION").unwrap_or(git_version::git_version!( - prefix = "git:", - cargo_prefix = "cargo:", - fallback = "unknown" - )) + self.garage.system.garage_version(), ) .unwrap(); diff --git a/src/garage/cli/layout.rs b/src/garage/cli/layout.rs index 0247c32b..db0af57c 100644 --- a/src/garage/cli/layout.rs +++ b/src/garage/cli/layout.rs @@ -1,5 +1,4 @@ use garage_util::crdt::Crdt; -use garage_util::data::*; use garage_util::error::*; use garage_util::formater::format_table; @@ -212,31 +211,9 @@ pub async fn cmd_apply_layout( rpc_host: NodeID, apply_opt: ApplyLayoutOpt, ) -> Result<(), Error> { - let mut layout = fetch_layout(rpc_cli, rpc_host).await?; + let layout = fetch_layout(rpc_cli, rpc_host).await?; - match apply_opt.version { - None => { - println!("Please pass the --version flag to ensure that you are writing the correct version of the cluster layout."); - println!("To know the correct value of the --version flag, invoke `garage layout show` and review the proposed changes."); - return Err(Error::Message("--version flag is missing".into())); - } - Some(v) => { - if v != layout.version + 1 { - return Err(Error::Message("Invalid value of --version flag".into())); - } - } - } - - layout.roles.merge(&layout.staging); - - if !layout.calculate_partition_assignation() { - return Err(Error::Message("Could not calculate new assignation of partitions to nodes. This can happen if there are less nodes than the desired number of copies of your data (see the replication_mode configuration parameter).".into())); - } - - layout.staging.clear(); - layout.staging_hash = blake2sum(&rmp_to_vec_all_named(&layout.staging).unwrap()[..]); - - layout.version += 1; + let layout = layout.apply_staged_changes(apply_opt.version)?; send_layout(rpc_cli, rpc_host, layout).await?; @@ -251,25 +228,9 @@ pub async fn cmd_revert_layout( rpc_host: NodeID, revert_opt: RevertLayoutOpt, ) -> Result<(), Error> { - let mut layout = fetch_layout(rpc_cli, rpc_host).await?; + let layout = fetch_layout(rpc_cli, rpc_host).await?; - match revert_opt.version { - None => { - println!("Please pass the --version flag to ensure that you are writing the correct version of the cluster layout."); - println!("To know the correct value of the --version flag, invoke `garage layout show` and review the proposed changes."); - return Err(Error::Message("--version flag is missing".into())); - } - Some(v) => { - if v != layout.version + 1 { - return Err(Error::Message("Invalid value of --version flag".into())); - } - } - } - - layout.staging.clear(); - layout.staging_hash = blake2sum(&rmp_to_vec_all_named(&layout.staging).unwrap()[..]); - - layout.version += 1; + let layout = layout.revert_staged_changes(revert_opt.version)?; send_layout(rpc_cli, rpc_host, layout).await?; diff --git a/src/garage/main.rs b/src/garage/main.rs index e898e680..bd09b6ea 100644 --- a/src/garage/main.rs +++ b/src/garage/main.rs @@ -8,6 +8,7 @@ mod admin; mod cli; mod repair; mod server; +mod tracing_setup; use std::net::SocketAddr; use std::path::PathBuf; @@ -141,6 +142,7 @@ async fn cli_command(opt: Opt) -> Result<(), Error> { match cli_command_dispatch(opt.cmd, &system_rpc_endpoint, &admin_rpc_endpoint, id).await { Err(HelperError::Internal(i)) => Err(Error::Message(format!("Internal error: {}", i))), Err(HelperError::BadRequest(b)) => Err(Error::Message(b)), + Err(e) => Err(Error::Message(format!("{}", e))), Ok(x) => Ok(x), } } diff --git a/src/garage/server.rs b/src/garage/server.rs index 24bb25b3..b58ad286 100644 --- a/src/garage/server.rs +++ b/src/garage/server.rs @@ -6,8 +6,7 @@ use garage_util::background::*; use garage_util::config::*; use garage_util::error::Error; -use garage_admin::metrics::*; -use garage_admin::tracing_setup::*; +use garage_api::admin::api_server::AdminApiServer; use garage_api::s3::api_server::S3ApiServer; use garage_model::garage::Garage; use garage_web::run_web_server; @@ -16,6 +15,7 @@ use garage_web::run_web_server; use garage_api::k2v::api_server::K2VApiServer; use crate::admin::*; +use crate::tracing_setup::*; async fn wait_from(mut chan: watch::Receiver) { while !*chan.borrow() { @@ -39,9 +39,6 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> { .open() .expect("Unable to open sled DB"); - info!("Initialize admin web server and metric backend..."); - let admin_server_init = AdminServer::init(); - info!("Initializing background runner..."); let watch_cancel = netapp::util::watch_ctrl_c(); let (background, await_background_done) = BackgroundRunner::new(16, watch_cancel.clone()); @@ -54,6 +51,9 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> { init_tracing(&export_to, garage.system.id)?; } + info!("Initialize Admin API server and metrics collector..."); + let admin_server = AdminApiServer::new(garage.clone()); + let run_system = tokio::spawn(garage.system.clone().run(watch_cancel.clone())); info!("Create admin RPC handler..."); @@ -80,39 +80,41 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> { wait_from(watch_cancel.clone()), )); - let admin_server = if let Some(admin_bind_addr) = config.admin.api_bind_addr { - info!("Configure and run admin web server..."); - Some(tokio::spawn( - admin_server_init.run(admin_bind_addr, wait_from(watch_cancel.clone())), - )) - } else { - None - }; + info!("Launching Admin API server..."); + let admin_server = tokio::spawn(admin_server.run(wait_from(watch_cancel.clone()))); // Stuff runs // When a cancel signal is sent, stuff stops if let Err(e) = s3_api_server.await? { warn!("S3 API server exited with error: {}", e); + } else { + info!("S3 API server exited without error."); } #[cfg(feature = "k2v")] if let Err(e) = k2v_api_server.await? { warn!("K2V API server exited with error: {}", e); + } else { + info!("K2V API server exited without error."); } if let Err(e) = web_server.await? { warn!("Web server exited with error: {}", e); + } else { + info!("Web server exited without error."); } - if let Some(a) = admin_server { - if let Err(e) = a.await? { - warn!("Admin web server exited with error: {}", e); - } + if let Err(e) = admin_server.await? { + warn!("Admin web server exited with error: {}", e); + } else { + info!("Admin API server exited without error."); } // Remove RPC handlers for system to break reference cycles garage.system.netapp.drop_all_handlers(); + opentelemetry::global::shutdown_tracer_provider(); // Await for netapp RPC system to end run_system.await?; + info!("Netapp exited"); // Drop all references so that stuff can terminate properly drop(garage); diff --git a/src/admin/tracing_setup.rs b/src/garage/tracing_setup.rs similarity index 100% rename from src/admin/tracing_setup.rs rename to src/garage/tracing_setup.rs diff --git a/src/model/garage.rs b/src/model/garage.rs index 03e21f8a..2f99bd68 100644 --- a/src/model/garage.rs +++ b/src/model/garage.rs @@ -191,6 +191,10 @@ impl Garage { pub fn bucket_helper(&self) -> helper::bucket::BucketHelper { helper::bucket::BucketHelper(self) } + + pub fn key_helper(&self) -> helper::key::KeyHelper { + helper::key::KeyHelper(self) + } } #[cfg(feature = "k2v")] diff --git a/src/model/helper/bucket.rs b/src/model/helper/bucket.rs index 54d2f97b..130ba5be 100644 --- a/src/model/helper/bucket.rs +++ b/src/model/helper/bucket.rs @@ -1,15 +1,18 @@ -use garage_table::util::*; use garage_util::crdt::*; use garage_util::data::*; use garage_util::error::{Error as GarageError, OkOrMessage}; use garage_util::time::*; +use garage_table::util::*; + use crate::bucket_alias_table::*; use crate::bucket_table::*; use crate::garage::Garage; use crate::helper::error::*; -use crate::key_table::{Key, KeyFilter}; +use crate::helper::key::KeyHelper; +use crate::key_table::*; use crate::permission::BucketKeyPerm; +use crate::s3::object_table::ObjectFilter; pub struct BucketHelper<'a>(pub(crate) &'a Garage); @@ -49,6 +52,23 @@ impl<'a> BucketHelper<'a> { } } + #[allow(clippy::ptr_arg)] + pub async fn resolve_bucket(&self, bucket_name: &String, api_key: &Key) -> Result { + let api_key_params = api_key + .state + .as_option() + .ok_or_message("Key should not be deleted at this point")?; + + if let Some(Some(bucket_id)) = api_key_params.local_aliases.get(bucket_name) { + Ok(*bucket_id) + } else { + Ok(self + .resolve_global_bucket_name(bucket_name) + .await? + .ok_or_else(|| Error::NoSuchBucket(bucket_name.to_string()))?) + } + } + /// Returns a Bucket if it is present in bucket table, /// even if it is in deleted state. Querying a non-existing /// bucket ID returns an internal error. @@ -71,64 +91,7 @@ impl<'a> BucketHelper<'a> { .get(&EmptyKey, &bucket_id) .await? .filter(|b| !b.is_deleted()) - .ok_or_bad_request(format!( - "Bucket {:?} does not exist or has been deleted", - bucket_id - )) - } - - /// Returns a Key if it is present in key table, - /// even if it is in deleted state. Querying a non-existing - /// key ID returns an internal error. - pub async fn get_internal_key(&self, key_id: &String) -> Result { - Ok(self - .0 - .key_table - .get(&EmptyKey, key_id) - .await? - .ok_or_message(format!("Key {} does not exist", key_id))?) - } - - /// Returns a Key if it is present in key table, - /// only if it is in non-deleted state. - /// Querying a non-existing key ID or a deleted key - /// returns a bad request error. - pub async fn get_existing_key(&self, key_id: &String) -> Result { - self.0 - .key_table - .get(&EmptyKey, key_id) - .await? - .filter(|b| !b.state.is_deleted()) - .ok_or_bad_request(format!("Key {} does not exist or has been deleted", key_id)) - } - - /// Returns a Key if it is present in key table, - /// looking it up by key ID or by a match on its name, - /// only if it is in non-deleted state. - /// Querying a non-existing key ID or a deleted key - /// returns a bad request error. - pub async fn get_existing_matching_key(&self, pattern: &str) -> Result { - let candidates = self - .0 - .key_table - .get_range( - &EmptyKey, - None, - Some(KeyFilter::MatchesAndNotDeleted(pattern.to_string())), - 10, - EnumerationOrder::Forward, - ) - .await? - .into_iter() - .collect::>(); - if candidates.len() != 1 { - Err(Error::BadRequest(format!( - "{} matching keys", - candidates.len() - ))) - } else { - Ok(candidates.into_iter().next().unwrap()) - } + .ok_or_else(|| Error::NoSuchBucket(hex::encode(bucket_id))) } /// Sets a new alias for a bucket in global namespace. @@ -142,10 +105,7 @@ impl<'a> BucketHelper<'a> { alias_name: &String, ) -> Result<(), Error> { if !is_valid_bucket_name(alias_name) { - return Err(Error::BadRequest(format!( - "{}: {}", - alias_name, INVALID_BUCKET_NAME_MESSAGE - ))); + return Err(Error::InvalidBucketName(alias_name.to_string())); } let mut bucket = self.get_existing_bucket(bucket_id).await?; @@ -176,7 +136,7 @@ impl<'a> BucketHelper<'a> { let alias = match alias { None => BucketAlias::new(alias_name.clone(), alias_ts, Some(bucket_id)) - .ok_or_bad_request(format!("{}: {}", alias_name, INVALID_BUCKET_NAME_MESSAGE))?, + .ok_or_else(|| Error::InvalidBucketName(alias_name.clone()))?, Some(mut a) => { a.state = Lww::raw(alias_ts, Some(bucket_id)); a @@ -264,7 +224,7 @@ impl<'a> BucketHelper<'a> { .bucket_alias_table .get(&EmptyKey, alias_name) .await? - .ok_or_message(format!("Alias {} not found", alias_name))?; + .ok_or_else(|| Error::NoSuchBucket(alias_name.to_string()))?; // Checks ok, remove alias let alias_ts = match bucket.state.as_option() { @@ -303,15 +263,14 @@ impl<'a> BucketHelper<'a> { key_id: &String, alias_name: &String, ) -> Result<(), Error> { + let key_helper = KeyHelper(self.0); + if !is_valid_bucket_name(alias_name) { - return Err(Error::BadRequest(format!( - "{}: {}", - alias_name, INVALID_BUCKET_NAME_MESSAGE - ))); + return Err(Error::InvalidBucketName(alias_name.to_string())); } let mut bucket = self.get_existing_bucket(bucket_id).await?; - let mut key = self.get_existing_key(key_id).await?; + let mut key = key_helper.get_existing_key(key_id).await?; let mut key_param = key.state.as_option_mut().unwrap(); @@ -360,8 +319,10 @@ impl<'a> BucketHelper<'a> { key_id: &String, alias_name: &String, ) -> Result<(), Error> { + let key_helper = KeyHelper(self.0); + let mut bucket = self.get_existing_bucket(bucket_id).await?; - let mut key = self.get_existing_key(key_id).await?; + let mut key = key_helper.get_existing_key(key_id).await?; let mut bucket_p = bucket.state.as_option_mut().unwrap(); @@ -429,8 +390,10 @@ impl<'a> BucketHelper<'a> { key_id: &String, mut perm: BucketKeyPerm, ) -> Result<(), Error> { + let key_helper = KeyHelper(self.0); + let mut bucket = self.get_internal_bucket(bucket_id).await?; - let mut key = self.get_internal_key(key_id).await?; + let mut key = key_helper.get_internal_key(key_id).await?; if let Some(bstate) = bucket.state.as_option() { if let Some(kp) = bstate.authorized_keys.get(key_id) { @@ -466,4 +429,47 @@ impl<'a> BucketHelper<'a> { Ok(()) } + + pub async fn is_bucket_empty(&self, bucket_id: Uuid) -> Result { + let objects = self + .0 + .object_table + .get_range( + &bucket_id, + None, + Some(ObjectFilter::IsData), + 10, + EnumerationOrder::Forward, + ) + .await?; + if !objects.is_empty() { + return Ok(false); + } + + #[cfg(feature = "k2v")] + { + use garage_rpc::ring::Ring; + use std::sync::Arc; + + let ring: Arc = self.0.system.ring.borrow().clone(); + let k2vindexes = self + .0 + .k2v + .counter_table + .table + .get_range( + &bucket_id, + None, + Some((DeletedFilter::NotDeleted, ring.layout.node_id_vec.clone())), + 10, + EnumerationOrder::Forward, + ) + .await?; + if !k2vindexes.is_empty() { + return Ok(false); + } + } + + Ok(true) + } } diff --git a/src/model/helper/error.rs b/src/model/helper/error.rs index 30b2ba32..3ca8f55c 100644 --- a/src/model/helper/error.rs +++ b/src/model/helper/error.rs @@ -10,6 +10,16 @@ pub enum Error { #[error(display = "Bad request: {}", _0)] BadRequest(String), + + /// Bucket name is not valid according to AWS S3 specs + #[error(display = "Invalid bucket name: {}", _0)] + InvalidBucketName(String), + + #[error(display = "Access key not found: {}", _0)] + NoSuchAccessKey(String), + + #[error(display = "Bucket not found: {}", _0)] + NoSuchBucket(String), } impl From for Error { diff --git a/src/model/helper/key.rs b/src/model/helper/key.rs new file mode 100644 index 00000000..c1a8e974 --- /dev/null +++ b/src/model/helper/key.rs @@ -0,0 +1,102 @@ +use garage_table::util::*; +use garage_util::crdt::*; +use garage_util::error::OkOrMessage; + +use crate::garage::Garage; +use crate::helper::bucket::BucketHelper; +use crate::helper::error::*; +use crate::key_table::{Key, KeyFilter}; +use crate::permission::BucketKeyPerm; + +pub struct KeyHelper<'a>(pub(crate) &'a Garage); + +#[allow(clippy::ptr_arg)] +impl<'a> KeyHelper<'a> { + /// Returns a Key if it is present in key table, + /// even if it is in deleted state. Querying a non-existing + /// key ID returns an internal error. + pub async fn get_internal_key(&self, key_id: &String) -> Result { + Ok(self + .0 + .key_table + .get(&EmptyKey, key_id) + .await? + .ok_or_message(format!("Key {} does not exist", key_id))?) + } + + /// Returns a Key if it is present in key table, + /// only if it is in non-deleted state. + /// Querying a non-existing key ID or a deleted key + /// returns a bad request error. + pub async fn get_existing_key(&self, key_id: &String) -> Result { + self.0 + .key_table + .get(&EmptyKey, key_id) + .await? + .filter(|b| !b.state.is_deleted()) + .ok_or_else(|| Error::NoSuchAccessKey(key_id.to_string())) + } + + /// Returns a Key if it is present in key table, + /// looking it up by key ID or by a match on its name, + /// only if it is in non-deleted state. + /// Querying a non-existing key ID or a deleted key + /// returns a bad request error. + pub async fn get_existing_matching_key(&self, pattern: &str) -> Result { + let candidates = self + .0 + .key_table + .get_range( + &EmptyKey, + None, + Some(KeyFilter::MatchesAndNotDeleted(pattern.to_string())), + 10, + EnumerationOrder::Forward, + ) + .await? + .into_iter() + .collect::>(); + if candidates.len() != 1 { + Err(Error::BadRequest(format!( + "{} matching keys", + candidates.len() + ))) + } else { + Ok(candidates.into_iter().next().unwrap()) + } + } + + /// Deletes an API access key + pub async fn delete_key(&self, key: &mut Key) -> Result<(), Error> { + let bucket_helper = BucketHelper(self.0); + + let state = key.state.as_option_mut().unwrap(); + + // --- done checking, now commit --- + // (the step at unset_local_bucket_alias will fail if a bucket + // does not have another alias, the deletion will be + // interrupted in the middle if that happens) + + // 1. Delete local aliases + for (alias, _, to) in state.local_aliases.items().iter() { + if let Some(bucket_id) = to { + bucket_helper + .unset_local_bucket_alias(*bucket_id, &key.key_id, alias) + .await?; + } + } + + // 2. Remove permissions on all authorized buckets + for (ab_id, _auth) in state.authorized_buckets.items().iter() { + bucket_helper + .set_bucket_key_permissions(*ab_id, &key.key_id, BucketKeyPerm::NO_PERMISSIONS) + .await?; + } + + // 3. Actually delete key + key.state = Deletable::delete(); + self.0.key_table.insert(key).await?; + + Ok(()) + } +} diff --git a/src/model/helper/mod.rs b/src/model/helper/mod.rs index 2f4e8898..dd947c86 100644 --- a/src/model/helper/mod.rs +++ b/src/model/helper/mod.rs @@ -1,2 +1,3 @@ pub mod bucket; pub mod error; +pub mod key; diff --git a/src/rpc/Cargo.toml b/src/rpc/Cargo.toml index bed7f44a..73328993 100644 --- a/src/rpc/Cargo.toml +++ b/src/rpc/Cargo.toml @@ -15,11 +15,11 @@ path = "lib.rs" [dependencies] garage_util = { version = "0.7.0", path = "../util" } -garage_admin = { version = "0.7.0", path = "../admin" } arc-swap = "1.0" bytes = "1.0" gethostname = "0.2" +git-version = "0.3.4" hex = "0.4" tracing = "0.1.30" rand = "0.8" diff --git a/src/rpc/layout.rs b/src/rpc/layout.rs index b9c02c21..f517f36f 100644 --- a/src/rpc/layout.rs +++ b/src/rpc/layout.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use garage_util::crdt::{AutoCrdt, Crdt, LwwMap}; use garage_util::data::*; +use garage_util::error::*; use crate::ring::*; @@ -100,6 +101,61 @@ impl ClusterLayout { } } + pub fn apply_staged_changes(mut self, version: Option) -> Result { + match version { + None => { + let error = r#" +Please pass the new layout version number to ensure that you are writing the correct version of the cluster layout. +To know the correct value of the new layout version, invoke `garage layout show` and review the proposed changes. + "#; + return Err(Error::Message(error.into())); + } + Some(v) => { + if v != self.version + 1 { + return Err(Error::Message("Invalid new layout version".into())); + } + } + } + + self.roles.merge(&self.staging); + self.roles.retain(|(_, _, v)| v.0.is_some()); + + if !self.calculate_partition_assignation() { + return Err(Error::Message("Could not calculate new assignation of partitions to nodes. This can happen if there are less nodes than the desired number of copies of your data (see the replication_mode configuration parameter).".into())); + } + + self.staging.clear(); + self.staging_hash = blake2sum(&rmp_to_vec_all_named(&self.staging).unwrap()[..]); + + self.version += 1; + + Ok(self) + } + + pub fn revert_staged_changes(mut self, version: Option) -> Result { + match version { + None => { + let error = r#" +Please pass the new layout version number to ensure that you are writing the correct version of the cluster layout. +To know the correct value of the new layout version, invoke `garage layout show` and review the proposed changes. + "#; + return Err(Error::Message(error.into())); + } + Some(v) => { + if v != self.version + 1 { + return Err(Error::Message("Invalid new layout version".into())); + } + } + } + + self.staging.clear(); + self.staging_hash = blake2sum(&rmp_to_vec_all_named(&self.staging).unwrap()[..]); + + self.version += 1; + + Ok(self) + } + /// Returns a list of IDs of nodes that currently have /// a role in the cluster pub fn node_ids(&self) -> &[Uuid] { diff --git a/src/rpc/system.rs b/src/rpc/system.rs index 68d94ea5..1d7c3ea4 100644 --- a/src/rpc/system.rs +++ b/src/rpc/system.rs @@ -312,6 +312,84 @@ impl System { ); } + // ---- Administrative operations (directly available and + // also available through RPC) ---- + + pub fn garage_version(&self) -> &'static str { + option_env!("GIT_VERSION").unwrap_or(git_version::git_version!( + prefix = "git:", + cargo_prefix = "cargo:", + fallback = "unknown" + )) + } + + pub fn get_known_nodes(&self) -> Vec { + let node_status = self.node_status.read().unwrap(); + let known_nodes = self + .fullmesh + .get_peer_list() + .iter() + .map(|n| KnownNodeInfo { + id: n.id.into(), + addr: n.addr, + is_up: n.is_up(), + last_seen_secs_ago: n.last_seen.map(|t| (Instant::now() - t).as_secs()), + status: node_status + .get(&n.id.into()) + .cloned() + .map(|(_, st)| st) + .unwrap_or(NodeStatus { + hostname: "?".to_string(), + replication_factor: 0, + cluster_layout_version: 0, + cluster_layout_staging_hash: Hash::from([0u8; 32]), + }), + }) + .collect::>(); + known_nodes + } + + pub fn get_cluster_layout(&self) -> ClusterLayout { + self.ring.borrow().layout.clone() + } + + pub async fn update_cluster_layout( + self: &Arc, + layout: &ClusterLayout, + ) -> Result<(), Error> { + self.handle_advertise_cluster_layout(layout).await?; + Ok(()) + } + + pub async fn connect(&self, node: &str) -> Result<(), Error> { + let (pubkey, addrs) = parse_and_resolve_peer_addr(node).ok_or_else(|| { + Error::Message(format!( + "Unable to parse or resolve node specification: {}", + node + )) + })?; + let mut errors = vec![]; + for ip in addrs.iter() { + match self + .netapp + .clone() + .try_connect(*ip, pubkey) + .await + .err_context(CONNECT_ERROR_MESSAGE) + { + Ok(()) => return Ok(()), + Err(e) => { + errors.push((*ip, e)); + } + } + } + if errors.len() == 1 { + Err(Error::Message(errors[0].1.to_string())) + } else { + Err(Error::Message(format!("{:?}", errors))) + } + } + // ---- INTERNALS ---- async fn advertise_to_consul(self: Arc) -> Result<(), Error> { @@ -384,32 +462,11 @@ impl System { self.local_status.swap(Arc::new(new_si)); } + // --- RPC HANDLERS --- + async fn handle_connect(&self, node: &str) -> Result { - let (pubkey, addrs) = parse_and_resolve_peer_addr(node).ok_or_else(|| { - Error::Message(format!( - "Unable to parse or resolve node specification: {}", - node - )) - })?; - let mut errors = vec![]; - for ip in addrs.iter() { - match self - .netapp - .clone() - .try_connect(*ip, pubkey) - .await - .err_context(CONNECT_ERROR_MESSAGE) - { - Ok(()) => return Ok(SystemRpc::Ok), - Err(e) => { - errors.push((*ip, e)); - } - } - } - return Err(Error::Message(format!( - "Could not connect to specified peers. Errors: {:?}", - errors - ))); + self.connect(node).await?; + Ok(SystemRpc::Ok) } fn handle_pull_cluster_layout(&self) -> SystemRpc { @@ -418,28 +475,7 @@ impl System { } fn handle_get_known_nodes(&self) -> SystemRpc { - let node_status = self.node_status.read().unwrap(); - let known_nodes = self - .fullmesh - .get_peer_list() - .iter() - .map(|n| KnownNodeInfo { - id: n.id.into(), - addr: n.addr, - is_up: n.is_up(), - last_seen_secs_ago: n.last_seen.map(|t| (Instant::now() - t).as_secs()), - status: node_status - .get(&n.id.into()) - .cloned() - .map(|(_, st)| st) - .unwrap_or(NodeStatus { - hostname: "?".to_string(), - replication_factor: 0, - cluster_layout_version: 0, - cluster_layout_staging_hash: Hash::from([0u8; 32]), - }), - }) - .collect::>(); + let known_nodes = self.get_known_nodes(); SystemRpc::ReturnKnownNodes(known_nodes) } @@ -476,7 +512,7 @@ impl System { } async fn handle_advertise_cluster_layout( - self: Arc, + self: &Arc, adv: &ClusterLayout, ) -> Result { let update_ring = self.update_ring.lock().await; diff --git a/src/util/config.rs b/src/util/config.rs index 4d66bfe4..99ebce31 100644 --- a/src/util/config.rs +++ b/src/util/config.rs @@ -121,6 +121,10 @@ pub struct WebConfig { pub struct AdminConfig { /// Address and port to bind for admin API serving pub api_bind_addr: Option, + /// Bearer token to use to scrape metrics + pub metrics_token: Option, + /// Bearer token to use to access Admin API endpoints + pub admin_token: Option, /// OTLP server to where to export traces pub trace_sink: Option, } diff --git a/src/util/crdt/lww_map.rs b/src/util/crdt/lww_map.rs index c155c3a8..91d24c7f 100644 --- a/src/util/crdt/lww_map.rs +++ b/src/util/crdt/lww_map.rs @@ -140,6 +140,11 @@ where self.vals.clear(); } + /// Retain only values that match a certain predicate + pub fn retain(&mut self, pred: impl FnMut(&(K, u64, V)) -> bool) { + self.vals.retain(pred); + } + /// Get a reference to the value assigned to a key pub fn get(&self, k: &K) -> Option<&V> { match self.vals.binary_search_by(|(k2, _, _)| k2.cmp(k)) { diff --git a/src/web/error.rs b/src/web/error.rs index 55990e9d..bd8f17b5 100644 --- a/src/web/error.rs +++ b/src/web/error.rs @@ -2,57 +2,47 @@ use err_derive::Error; use hyper::header::HeaderValue; use hyper::{HeaderMap, StatusCode}; -use garage_util::error::Error as GarageError; +use garage_api::generic_server::ApiError; /// Errors of this crate #[derive(Debug, Error)] pub enum Error { /// An error received from the API crate #[error(display = "API error: {}", _0)] - ApiError(#[error(source)] garage_api::Error), - - // Category: internal error - /// Error internal to garage - #[error(display = "Internal error: {}", _0)] - InternalError(#[error(source)] GarageError), + ApiError(garage_api::s3::error::Error), /// The file does not exist #[error(display = "Not found")] NotFound, - /// The request contained an invalid UTF-8 sequence in its path or in other parameters - #[error(display = "Invalid UTF-8: {}", _0)] - InvalidUtf8(#[error(source)] std::str::Utf8Error), - - /// The client send a header with invalid value - #[error(display = "Invalid header value: {}", _0)] - InvalidHeader(#[error(source)] hyper::header::ToStrError), - /// The client sent a request without host, or with unsupported method #[error(display = "Bad request: {}", _0)] BadRequest(String), } +impl From for Error +where + garage_api::s3::error::Error: From, +{ + fn from(err: T) -> Self { + Error::ApiError(garage_api::s3::error::Error::from(err)) + } +} + impl Error { /// Transform errors into http status code pub fn http_status_code(&self) -> StatusCode { match self { Error::NotFound => StatusCode::NOT_FOUND, Error::ApiError(e) => e.http_status_code(), - Error::InternalError( - GarageError::Timeout - | GarageError::RemoteError(_) - | GarageError::Quorum(_, _, _, _), - ) => StatusCode::SERVICE_UNAVAILABLE, - Error::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, - _ => StatusCode::BAD_REQUEST, + Error::BadRequest(_) => StatusCode::BAD_REQUEST, } } pub fn add_headers(&self, header_map: &mut HeaderMap) { #[allow(clippy::single_match)] match self { - Error::ApiError(e) => e.add_headers(header_map), + Error::ApiError(e) => e.add_http_headers(header_map), _ => (), } } diff --git a/src/web/web_server.rs b/src/web/web_server.rs index 867adc51..c30d8957 100644 --- a/src/web/web_server.rs +++ b/src/web/web_server.rs @@ -18,9 +18,11 @@ use opentelemetry::{ use crate::error::*; -use garage_api::error::{Error as ApiError, OkOrBadRequest, OkOrInternalError}; use garage_api::helpers::{authority_to_host, host_to_bucket}; use garage_api::s3::cors::{add_cors_headers, find_matching_cors_rule, handle_options_for_bucket}; +use garage_api::s3::error::{ + CommonErrorDerivative, Error as ApiError, OkOrBadRequest, OkOrInternalError, +}; use garage_api::s3::get::{handle_get, handle_head}; use garage_model::garage::Garage; @@ -207,7 +209,7 @@ async fn serve_file(garage: Arc, req: &Request) -> Result handle_options_for_bucket(req, &bucket), Method::HEAD => handle_head(garage.clone(), req, bucket_id, &key, None).await, Method::GET => handle_get(garage.clone(), req, bucket_id, &key, None).await, - _ => Err(ApiError::BadRequest("HTTP method not supported".into())), + _ => Err(ApiError::bad_request("HTTP method not supported")), } .map_err(Error::from); @@ -290,9 +292,7 @@ fn path_to_key<'a>(path: &'a str, index: &str) -> Result, Error> { let path_utf8 = percent_encoding::percent_decode_str(path).decode_utf8()?; if !path_utf8.starts_with('/') { - return Err(Error::BadRequest( - "Path must start with a / (slash)".to_string(), - )); + return Err(Error::BadRequest("Path must start with a / (slash)".into())); } match path_utf8.chars().last() { From b2a2d3859fefd53dab0b87274d5aed1f6bb608a3 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 24 May 2022 12:48:05 +0200 Subject: [PATCH 009/149] K2V client improvements (#307) - [x] Better distinguish error types - [x] Parse error messages received from server - [x] Remove `src/` folder layer, we don't have that for other crates Co-authored-by: Alex Auvolat Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/307 Co-authored-by: Alex Co-committed-by: Alex --- Cargo.lock | 1 + Cargo.nix | 5 ++- src/k2v-client/Cargo.toml | 5 +++ src/k2v-client/{src => }/bin/k2v-cli.rs | 0 src/k2v-client/{src => }/error.rs | 7 ++++ src/k2v-client/{src => }/lib.rs | 53 +++++++++++++++++++++++-- 6 files changed, 65 insertions(+), 6 deletions(-) rename src/k2v-client/{src => }/bin/k2v-cli.rs (100%) rename src/k2v-client/{src => }/error.rs (81%) rename src/k2v-client/{src => }/lib.rs (91%) diff --git a/Cargo.lock b/Cargo.lock index fcf3030a..630642ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1584,6 +1584,7 @@ dependencies = [ "clap 3.1.18", "garage_util 0.7.0", "http", + "log", "rusoto_core", "rusoto_credential", "rusoto_signature", diff --git a/Cargo.nix b/Cargo.nix index 371ce8d3..d100e7bb 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -688,7 +688,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -2117,6 +2117,7 @@ in clap = rustPackages."registry+https://github.com/rust-lang/crates.io-index".clap."3.1.18" { inherit profileName; }; garage_util = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; + log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; rusoto_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rusoto_core."0.48.0" { inherit profileName; }; rusoto_credential = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rusoto_credential."0.48.0" { inherit profileName; }; rusoto_signature = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rusoto_signature."0.48.0" { inherit profileName; }; @@ -5029,7 +5030,7 @@ in [ "default" ] ]; dependencies = { - ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; diff --git a/src/k2v-client/Cargo.toml b/src/k2v-client/Cargo.toml index 84c6b8b2..224414ab 100644 --- a/src/k2v-client/Cargo.toml +++ b/src/k2v-client/Cargo.toml @@ -6,6 +6,7 @@ edition = "2018" [dependencies] base64 = "0.13.0" http = "0.2.6" +log = "0.4" rusoto_core = "0.48.0" rusoto_credential = "0.48.0" rusoto_signature = "0.48.0" @@ -22,6 +23,10 @@ garage_util = { path = "../util", optional = true } [features] cli = ["clap", "tokio/fs", "tokio/io-std", "garage_util"] +[lib] +path = "lib.rs" + [[bin]] name = "k2v-cli" +path = "bin/k2v-cli.rs" required-features = ["cli"] diff --git a/src/k2v-client/src/bin/k2v-cli.rs b/src/k2v-client/bin/k2v-cli.rs similarity index 100% rename from src/k2v-client/src/bin/k2v-cli.rs rename to src/k2v-client/bin/k2v-cli.rs diff --git a/src/k2v-client/src/error.rs b/src/k2v-client/error.rs similarity index 81% rename from src/k2v-client/src/error.rs rename to src/k2v-client/error.rs index 62357934..37c221f2 100644 --- a/src/k2v-client/src/error.rs +++ b/src/k2v-client/error.rs @@ -5,6 +5,13 @@ use thiserror::Error; /// Errors returned by this crate #[derive(Error, Debug)] pub enum Error { + #[error("{0}, {1}: {2} (path = {3})")] + Remote( + http::StatusCode, + Cow<'static, str>, + Cow<'static, str>, + Cow<'static, str>, + ), #[error("received invalid response: {0}")] InvalidResponse(Cow<'static, str>), #[error("not found")] diff --git a/src/k2v-client/src/lib.rs b/src/k2v-client/lib.rs similarity index 91% rename from src/k2v-client/src/lib.rs rename to src/k2v-client/lib.rs index ba1cd6ea..95974d7a 100644 --- a/src/k2v-client/src/lib.rs +++ b/src/k2v-client/lib.rs @@ -4,6 +4,7 @@ use std::time::Duration; use http::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE}; use http::status::StatusCode; use http::HeaderMap; +use log::{debug, error}; use rusoto_core::{ByteStream, DispatchSignedRequest, HttpClient}; use rusoto_credential::AwsCredentials; @@ -310,12 +311,47 @@ impl K2vClient { StatusCode::NO_CONTENT => Vec::new(), StatusCode::NOT_FOUND => return Err(Error::NotFound), StatusCode::NOT_MODIFIED => Vec::new(), - _ => { - return Err(Error::InvalidResponse( - format!("invalid error code: {}", res.status).into(), - )) + s => { + let err_body = read_body(&mut res.headers, res.body) + .await + .unwrap_or_default(); + let err_body_str = std::str::from_utf8(&err_body) + .map(String::from) + .unwrap_or_else(|_| base64::encode(&err_body)); + + if s.is_client_error() || s.is_server_error() { + error!("Error response {}: {}", res.status, err_body_str); + let err = match serde_json::from_slice::(&err_body) { + Ok(err) => Error::Remote( + res.status, + err.code.into(), + err.message.into(), + err.path.into(), + ), + Err(_) => Error::Remote( + res.status, + "unknown".into(), + err_body_str.into(), + "?".into(), + ), + }; + return Err(err); + } else { + let msg = format!( + "Unexpected response code {}. Response body: {}", + res.status, err_body_str + ); + error!("{}", msg); + return Err(Error::InvalidResponse(msg.into())); + } } }; + debug!( + "Response body: {}", + std::str::from_utf8(&body) + .map(String::from) + .unwrap_or_else(|_| base64::encode(&body)) + ); Ok(Response { body, @@ -558,6 +594,15 @@ struct BatchDeleteResponse<'a> { deleted_items: u64, } +#[derive(Deserialize)] +struct ErrorResponse { + code: String, + message: String, + #[allow(dead_code)] + region: String, + path: String, +} + struct Response { body: Vec, status: StatusCode, From 2da448b43f3427700e5f59e8f16f507aa2e1f372 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 24 May 2022 15:28:37 +0200 Subject: [PATCH 010/149] Add documentation for new Admin API and a few infos on K2V --- .../reference-manual}/admin-api.md | 29 +++++++++- doc/book/reference-manual/configuration.md | 36 ++++++++++-- doc/book/reference-manual/k2v.md | 58 +++++++++++++++++++ 3 files changed, 116 insertions(+), 7 deletions(-) rename doc/{drafts => book/reference-manual}/admin-api.md (93%) create mode 100644 doc/book/reference-manual/k2v.md diff --git a/doc/drafts/admin-api.md b/doc/book/reference-manual/admin-api.md similarity index 93% rename from doc/drafts/admin-api.md rename to doc/book/reference-manual/admin-api.md index b35a87f1..a891da07 100644 --- a/doc/drafts/admin-api.md +++ b/doc/book/reference-manual/admin-api.md @@ -1,18 +1,41 @@ -# Specification of Garage's administration API ++++ +title = "Specification of Garage's administration API" +weight = 16 ++++ +The Garage administration API is accessible through a dedicated server whose +listen address is specified in the `[admin]` section of the configuration +file (see [configuration file +reference](@/documentation/reference-manual/configuration.md)) **WARNING.** At this point, there is no comittement to stability of the APIs described in this document. We will bump the version numbers prefixed to each API endpoint at each time the syntax or semantics change, meaning that code that relies on these endpoint will break when changes are introduced. +The Garage administration API was introduced in version 0.7.2, this document +does not apply to older versions of Garage. + ## Access control The admin API uses two different tokens for acces control, that are specified in the config file's `[admin]` section: -- `metrics_token`: the token for accessing the Metrics endpoint (if this token is not set in the config file, the Metrics endpoint can be accessed without access control); -- `admin_token`: the token for accessing all of the other administration endpoints (if this token is not set in the config file, access to these endpoints is disabled entirely). +- `metrics_token`: the token for accessing the Metrics endpoint (if this token + is not set in the config file, the Metrics endpoint can be accessed without + access control); + +- `admin_token`: the token for accessing all of the other administration + endpoints (if this token is not set in the config file, access to these + endpoints is disabled entirely). + +These tokens are used as simple HTTP bearer tokens. In other words, to +authenticate access to an admin API endpoint, add the following HTTP header +to your request: + +``` +Authorization: Bearer +``` ## Administration API endpoints diff --git a/doc/book/reference-manual/configuration.md b/doc/book/reference-manual/configuration.md index bb04650c..65381f46 100644 --- a/doc/book/reference-manual/configuration.md +++ b/doc/book/reference-manual/configuration.md @@ -10,6 +10,7 @@ metadata_dir = "/var/lib/garage/meta" data_dir = "/var/lib/garage/data" block_size = 1048576 +block_manager_background_tranquility = 2 replication_mode = "3" @@ -47,6 +48,8 @@ root_domain = ".web.garage" [admin] api_bind_addr = "0.0.0.0:3903" +metrics_token = "cacce0b2de4bc2d9f5b5fdff551e01ac1496055aed248202d415398987e35f81" +admin_token = "ae8cb40ea7368bbdbb6430af11cca7da833d3458a5f52086f4e805a570fb5c2a" trace_sink = "http://localhost:4317" ``` @@ -84,6 +87,17 @@ files will remain available. This however means that chunks from existing files will not be deduplicated with chunks from newly uploaded files, meaning you might use more storage space that is optimally possible. +### `block_manager_background_tranquility` + +This parameter tunes the activity of the background worker responsible for +resyncing data blocks between nodes. The higher the tranquility value is set, +the more the background worker will wait between iterations, meaning the load +on the system (including network usage between nodes) will be reduced. The +minimal value for this parameter is `0`, where the background worker will +allways work at maximal throughput to resynchronize blocks. The default value +is `2`, where the background worker will try to spend at most 1/3 of its time +working, and 2/3 sleeping in order to reduce system load. + ### `replication_mode` Garage supports the following replication modes: @@ -326,10 +340,24 @@ Garage has a few administration capabilities, in particular to allow remote moni ### `api_bind_addr` If specified, Garage will bind an HTTP server to this port and address, on -which it will listen to requests for administration features. Currently, -this endpoint only exposes Garage metrics in the Prometheus format at -`/metrics`. This endpoint is not authenticated. In the future, bucket and -access key management might be possible by REST calls to this endpoint. +which it will listen to requests for administration features. +See [administration API reference](@/documentation/reference-manual/admin-api.md) to learn more about these features. + +### `metrics_token` (since version 0.7.2) + +The token for accessing the Metrics endpoint. If this token is not set in +the config file, the Metrics endpoint can be accessed without access +control. + +You can use any random string for this value. We recommend generating a random token with `openssl rand -hex 32`. + +### `admin_token` (since version 0.7.2) + +The token for accessing all of the other administration endpoints. If this +token is not set in the config file, access to these endpoints is disabled +entirely. + +You can use any random string for this value. We recommend generating a random token with `openssl rand -hex 32`. ### `trace_sink` diff --git a/doc/book/reference-manual/k2v.md b/doc/book/reference-manual/k2v.md new file mode 100644 index 00000000..742e4309 --- /dev/null +++ b/doc/book/reference-manual/k2v.md @@ -0,0 +1,58 @@ ++++ +title = "K2V" +weight = 30 ++++ + +Starting with version 0.7.2, Garage introduces an optionnal feature, K2V, +which is an alternative storage API designed to help efficiently store +many small values in buckets (in opposition to S3 which is more designed +to store large blobs). + +K2V is currently disabled at compile time in all builds, as the +specification is still subject to changes. To build a Garage version with +K2V, the Cargo feature flag `k2v` must be activated. Special builds with +the `k2v` feature flag enabled can be obtained from our download page under +"Extra builds": such builds can be identified easily as their tag name ends +with `-k2v` (example: `v0.7.2-k2v`). + +The specification of the K2V API can be found +[here](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/k2v/doc/drafts/k2v-spec.md). +This document also includes a high-level overview of K2V's design. + +The K2V API uses AWSv4 signatures for authentification, same as the S3 API. +The AWS region used for signature calculation is always the same as the one +defined for the S3 API in the config file. + +## Enabling and using K2V + +To enable K2V, download and run a build that has the `k2v` feature flag +enabled, or produce one yourself. Then, add the following section to your +configuration file: + +```toml +[k2v_api] +api_bind_addr = ":" +``` + +Please select a port number that is not already in use by another API +endpoint (S3 api, admin API) or by the RPC server. + +We provide an early-stage K2V client library for Rust which can be imported by adding the following to your `Cargo.toml` file: + +```toml +k2v-client = { git = "https://git.deuxfleurs.fr/Deuxfleurs/garage.git" } +``` + +There is also a simple CLI utility which can be built from source in the +following way: + +```sh +git clone https://git.deuxfleurs.fr/Deuxfleurs/garage.git +cd garage/src/k2v-client +cargo build --features cli --bin k2v-cli +``` + +The CLI utility is self-documented, run `k2v-cli --help` to learn how to use +it. There is also a short README.md in the `src/k2v-client` folder with some +instructions. + From 3be43f3372b33a95b1c033ae9bf8c674ea796d52 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 24 May 2022 15:32:42 +0200 Subject: [PATCH 011/149] Add lost content for Restic with Garage Suggested-by: Quentin --- doc/book/connect/backup.md | 55 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/doc/book/connect/backup.md b/doc/book/connect/backup.md index 5110442c..48a2d7be 100644 --- a/doc/book/connect/backup.md +++ b/doc/book/connect/backup.md @@ -17,6 +17,61 @@ If you still want to use Borg, you can use it with `rclone mount`. ## Restic +Create your key and bucket: + +```bash +garage key new my-key +garage bucket create backup +garage bucket allow backup --read --write --key my-key +``` + +Then register your Key ID and Secret key in your environment: + +```bash +export AWS_ACCESS_KEY_ID=GKxxx +export AWS_SECRET_ACCESS_KEY=xxxx +``` + +Configure restic from environment too: + +```bash +export RESTIC_REPOSITORY="s3:http://localhost:3900/backups" + +echo "Generated password (save it safely): $(openssl rand -base64 32)" +export RESTIC_PASSWORD=xxx # copy paste your generated password here +``` + +Do not forget to save your password safely (in your password manager or print it). It will be needed to decrypt your backups. + +Now you can use restic: + +```bash +# Initialize the bucket, must be run once +restic init + +# Backup your PostgreSQL database +# (We suppose your PostgreSQL daemon is stopped for all commands) +restic backup /var/lib/postgresql + +# Show backup history +restic snapshots + +# Backup again your PostgreSQL database, it will be faster as only changes will be uploaded +restic backup /var/lib/postgresql + +# Show backup history (again) +restic snapshots + +# Restore a backup +# (79766175 is the ID of the snapshot you want to restore) +mv /var/lib/postgresql /var/lib/postgresql.broken +restic restore 79766175 --target /var/lib/postgresql +``` + +Restic has way more features than the ones presented here. +You can discover all of them by accessing its documentation from the link below. + + *External links:* [Restic Documentation > Amazon S3](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#amazon-s3) ## Duplicity From 9f303f6308a313c8310c3597ecb6828e46352821 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 24 May 2022 15:47:42 +0200 Subject: [PATCH 012/149] Shorter page title --- doc/book/reference-manual/admin-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/book/reference-manual/admin-api.md b/doc/book/reference-manual/admin-api.md index a891da07..b77f0d39 100644 --- a/doc/book/reference-manual/admin-api.md +++ b/doc/book/reference-manual/admin-api.md @@ -1,5 +1,5 @@ +++ -title = "Specification of Garage's administration API" +title = "Administration API" weight = 16 +++ From 43ddc933f9eb36a98369fe671005e35005d8e3cd Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 25 May 2022 15:20:08 +0200 Subject: [PATCH 013/149] Update Ceph S3 endpoints compatibility --- doc/book/reference-manual/s3-compatibility.md | 64 +++++++++++-------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/doc/book/reference-manual/s3-compatibility.md b/doc/book/reference-manual/s3-compatibility.md index 71b4c209..2a0b2ac7 100644 --- a/doc/book/reference-manual/s3-compatibility.md +++ b/doc/book/reference-manual/s3-compatibility.md @@ -3,18 +3,28 @@ title = "S3 Compatibility status" weight = 20 +++ +## DISCLAIMER + +**The compatibility list for other platforms is given only for information purposes and based on available documentation.** They are sometimes completed, in a best effort approach, with the source code and inputs from maintainers when documentation is lacking. We are not proactively monitoring new versions of each software, check the modification history to know when the page has been updated for the last time. Some entries will be inexact or outdated: for any serious decision, you must make your own tests. +**The official documentation of each project can be accessed by clicking on the project name in the column header.** + +Feel free to open a PR to fix this table. Minio is missing because they do not provide a public S3 compatibility list. + +## Update history + + - 2022-02-07 - First version of this page + - 2022-05-25 - Many Ceph S3 endpoints are not documented but implemented. Following a notification from the Ceph community, we added them. + ## Endpoint implementation All APIs that are missing on Garage will return a 501 Not Implemented. Some `x-amz-` headers are not implemented. -*The compatibility list for other platforms is given only for information purposes and based on available documentation. Some entries might be inexact. Feel free to open a PR to fix this table. Minio is missing because they do not provide a public S3 compatibility list.* - ### Features | Feature | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | |------------------------------|----------------------------------|-----------------|---------------|---------|-----| -| [signature v2](https://docs.aws.amazon.com/general/latest/gr/signature-version-2.html) (deprecated) | ❌ Missing | ✅ | ❌ | ✅ | ✅ | +| [signature v2](https://docs.aws.amazon.com/general/latest/gr/signature-version-2.html) (deprecated) | ❌ Missing | ✅ | ✅ | ✅ | ✅ | | [signature v4](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html) | ✅ Implemented | ✅ | ✅ | ❌ | ✅ | | [URL path-style](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html#path-style-access) (eg. `host.tld/bucket/key`) | ✅ Implemented | ✅ | ✅ | ❓| ✅ | | [URL vhost-style](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html#virtual-hosted-style-access) URL (eg. `bucket.host.tld/key`) | ✅ Implemented | ❌| ✅| ✅ | ✅ | @@ -37,7 +47,7 @@ Some `x-amz-` headers are not implemented. | [DeleteObjects](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html) | ✅ Implemented | ✅ | ✅ | ✅ | ✅ | | [GetObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html) | ✅ Implemented | ✅ | ✅ | ✅ | ✅ | | [ListObjects](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjects.html) | ✅ Implemented (see details below) | ✅ | ✅ | ✅ | ❌| -| [ListObjectsV2](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html) | ✅ Implemented | ❌| ❌| ❌| ✅ | +| [ListObjectsV2](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html) | ✅ Implemented | ❌| ✅ | ❌| ✅ | | [PostObject](https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPOST.html) (compatibility API) | ❌ Missing | ❌| ✅ | ❌| ❌| | [PutObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html) | ✅ Implemented | ✅ | ✅ | ✅ | ✅ | @@ -67,9 +77,9 @@ For more information, please refer to our [issue tracker](https://git.deuxfleurs | [DeleteBucketWebsite](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketWebsite.html) | ✅ Implemented | ❌| ❌| ❌| ❌| | [GetBucketWebsite](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketWebsite.html) | ✅ Implemented | ❌ | ❌| ❌| ❌| | [PutBucketWebsite](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketWebsite.html) | ⚠ Partially implemented (see below)| ❌| ❌| ❌| ❌| -| [DeleteBucketCors](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketCors.html) | ✅ Implemented | ❌| ❌| ❌| ✅ | -| [GetBucketCors](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketCors.html) | ✅ Implemented | ❌ | ❌| ❌| ✅ | -| [PutBucketCors](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketCors.html) | ✅ Implemented | ❌| ❌| ❌| ✅ | +| [DeleteBucketCors](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketCors.html) | ✅ Implemented | ❌| ✅ | ❌| ✅ | +| [GetBucketCors](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketCors.html) | ✅ Implemented | ❌ | ✅ | ❌| ✅ | +| [PutBucketCors](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketCors.html) | ✅ Implemented | ❌| ✅ | ❌| ✅ | **PutBucketWebsite:** Implemented, but only stores the index document suffix and the error document path. Redirects are not supported. @@ -83,16 +93,16 @@ See Garage CLI reference manual to learn how to use Garage's permission system. | Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | |------------------------------|----------------------------------|-----------------|---------------|---------|-----| -| [DeleteBucketPolicy](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketPolicy.html) | ❌ Missing | ❌| ❌| ✅ | ❌| -| [GetBucketPolicy](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketPolicy.html) | ❌ Missing | ❌| ❌| ⚠ | ❌| -| [GetBucketPolicyStatus](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketPolicyStatus.html) | ❌ Missing | ❌| ❌| ❌| ❌| -| [PutBucketPolicy](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketPolicy.html) | ❌ Missing | ❌| ❌| ⚠ | ❌| +| [DeleteBucketPolicy](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketPolicy.html) | ❌ Missing | ❌| ✅ | ✅ | ❌| +| [GetBucketPolicy](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketPolicy.html) | ❌ Missing | ❌| ✅ | ⚠ | ❌| +| [GetBucketPolicyStatus](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketPolicyStatus.html) | ❌ Missing | ❌| ✅ | ❌| ❌| +| [PutBucketPolicy](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketPolicy.html) | ❌ Missing | ❌| ✅ | ⚠ | ❌| | [GetBucketAcl](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketAcl.html) | ❌ Missing | ✅ | ✅ | ✅ | ✅ | | [PutBucketAcl](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketAcl.html) | ❌ Missing | ✅ | ✅ | ✅ | ✅ | | [GetObjectAcl](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectAcl.html) | ❌ Missing | ✅ | ✅ | ✅ | ✅ | | [PutObjectAcl](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectAcl.html) | ❌ Missing | ✅ | ✅ | ✅ | ✅ | -*Notes:* Ceph claims that it supports bucket policies but does not implement any Policy endpoints. They probably refer to their own permission system. Riak CS only supports a subset of the policy configuration. +*Notes:* Riak CS only supports a subset of the policy configuration. ### Versioning, Lifecycle endpoints @@ -102,8 +112,8 @@ If you need this feature, please [share your use case in our dedicated issue](ht | Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | |------------------------------|----------------------------------|-----------------|---------------|---------|-----| | [DeleteBucketLifecycle](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketLifecycle.html) | ❌ Missing | ❌| ✅| ❌| ✅| -| [GetBucketLifecycleConfiguration](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketLifecycleConfiguration.html) | ❌ Missing | ❌| ⚠ | ❌| ✅| -| [PutBucketLifecycleConfiguration](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketLifecycleConfiguration.html) | ❌ Missing | ❌| ⚠ | ❌| ✅| +| [GetBucketLifecycleConfiguration](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketLifecycleConfiguration.html) | ❌ Missing | ❌| ✅ | ❌| ✅| +| [PutBucketLifecycleConfiguration](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketLifecycleConfiguration.html) | ❌ Missing | ❌| ✅ | ❌| ✅| | [GetBucketVersioning](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketVersioning.html) | ❌ Stub (see below) | ✅| ✅ | ❌| ✅| | [ListObjectVersions](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectVersions.html) | ❌ Missing | ❌| ✅ | ❌| ✅| | [PutBucketVersioning](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketVersioning.html) | ❌ Missing | ❌| ✅| ❌| ✅| @@ -111,8 +121,6 @@ If you need this feature, please [share your use case in our dedicated issue](ht **GetBucketVersioning:** Stub implementation (Garage does not yet support versionning so this always returns "versionning not enabled"). -*Note: Ceph only supports `Expiration`, `NoncurrentVersionExpiration` and `AbortIncompleteMultipartUpload` on its Lifecycle endpoints.* - ### Replication endpoints Please open an issue if you have a use case for replication. @@ -135,8 +143,8 @@ Amazon defines a concept of [object locking](https://docs.aws.amazon.com/AmazonS | [PutObjectLegalHold](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectLegalHold.html) | ❌ Missing | ❌| ✅ | ❌| ❌| | [GetObjectRetention](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectRetention.html) | ❌ Missing | ❌| ✅ | ❌| ❌| | [PutObjectRetention](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectRetention.html) | ❌ Missing | ❌| ✅ | ❌| ❌| -| [GetObjectLockConfiguration](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectLockConfiguration.html) | ❌ Missing | ❌| ❌| ❌| ❌| -| [PutObjectLockConfiguration](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectLockConfiguration.html) | ❌ Missing | ❌| ❌| ❌| ❌| +| [GetObjectLockConfiguration](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectLockConfiguration.html) | ❌ Missing | ❌| ✅ | ❌| ❌| +| [PutObjectLockConfiguration](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectLockConfiguration.html) | ❌ Missing | ❌| ✅ | ❌| ❌| ### (Server-side) encryption @@ -145,9 +153,9 @@ Please open an issue if you have a use case. | Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | |------------------------------|----------------------------------|-----------------|---------------|---------|-----| -| [DeleteBucketEncryption](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketEncryption.html) | ❌ Missing | ❌| ❌| ❌| ❌| -| [GetBucketEncryption](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketEncryption.html) | ❌ Missing | ❌| ❌| ❌| ❌| -| [PutBucketEncryption](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketEncryption.html) | ❌ Missing | ❌| ❌| ❌| ❌| +| [DeleteBucketEncryption](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketEncryption.html) | ❌ Missing | ❌| ✅ | ❌| ❌| +| [GetBucketEncryption](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketEncryption.html) | ❌ Missing | ❌| ✅ | ❌| ❌| +| [PutBucketEncryption](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketEncryption.html) | ❌ Missing | ❌| ✅ | ❌| ❌| ### Misc endpoints @@ -155,13 +163,13 @@ Please open an issue if you have a use case. |------------------------------|----------------------------------|-----------------|---------------|---------|-----| | [GetBucketNotificationConfiguration](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketNotificationConfiguration.html) | ❌ Missing | ❌| ✅ | ❌| ❌| | [PutBucketNotificationConfiguration](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketNotificationConfiguration.html) | ❌ Missing | ❌| ✅ | ❌| ❌| -| [DeleteBucketTagging](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketTagging.html) | ❌ Missing | ❌| ❌| ❌| ✅ | -| [GetBucketTagging](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketTagging.html) | ❌ Missing | ❌| ❌| ❌| ✅ | -| [PutBucketTagging](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketTagging.html) | ❌ Missing | ❌| ❌| ❌| ✅ | -| [DeleteObjectTagging](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjectTagging.html) | ❌ Missing | ❌| ❌| ❌| ✅ | -| [GetObjectTagging](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectTagging.html) | ❌ Missing | ❌| ❌| ❌| ✅ | -| [PutObjectTagging](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectTagging.html) | ❌ Missing | ❌| ❌| ❌| ✅ | -| [GetObjectTorrent](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectTorrent.html) | ❌ Missing | ❌| ❌| ❌| ❌| +| [DeleteBucketTagging](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketTagging.html) | ❌ Missing | ❌| ✅ | ❌| ✅ | +| [GetBucketTagging](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketTagging.html) | ❌ Missing | ❌| ✅ | ❌| ✅ | +| [PutBucketTagging](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketTagging.html) | ❌ Missing | ❌| ✅ | ❌| ✅ | +| [DeleteObjectTagging](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjectTagging.html) | ❌ Missing | ❌| ✅ | ❌| ✅ | +| [GetObjectTagging](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectTagging.html) | ❌ Missing | ❌| ✅ | ❌| ✅ | +| [PutObjectTagging](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectTagging.html) | ❌ Missing | ❌| ✅ | ❌| ✅ | +| [GetObjectTorrent](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectTorrent.html) | ❌ Missing | ❌| ✅ | ❌| ❌| ### Vendor specific endpoints From 93eab8eaa3927b99626fee4a747a0f9f041cafdb Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 25 May 2022 16:54:44 +0200 Subject: [PATCH 014/149] Fixes to S3 compatibility page (#314) Mention PostObject is implemented, fix english mistakes Co-authored-by: Alex Auvolat Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/314 Co-authored-by: Alex Co-committed-by: Alex --- doc/book/reference-manual/s3-compatibility.md | 75 ++++++++++++------- 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/doc/book/reference-manual/s3-compatibility.md b/doc/book/reference-manual/s3-compatibility.md index 2a0b2ac7..a8e503d5 100644 --- a/doc/book/reference-manual/s3-compatibility.md +++ b/doc/book/reference-manual/s3-compatibility.md @@ -5,22 +5,26 @@ weight = 20 ## DISCLAIMER -**The compatibility list for other platforms is given only for information purposes and based on available documentation.** They are sometimes completed, in a best effort approach, with the source code and inputs from maintainers when documentation is lacking. We are not proactively monitoring new versions of each software, check the modification history to know when the page has been updated for the last time. Some entries will be inexact or outdated: for any serious decision, you must make your own tests. -**The official documentation of each project can be accessed by clicking on the project name in the column header.** +**The compatibility list for other platforms is given only for informational +purposes and based on available documentation.** They are sometimes completed, +in a best effort approach, with the source code and inputs from maintainers +when documentation is lacking. We are not proactively monitoring new versions +of each software: check the modification history to know when the page has been +updated for the last time. Some entries will be inexact or outdated. For any +serious decision, you must make your own tests. +**The official documentation of each project can be accessed by clicking on the +project name in the column header.** -Feel free to open a PR to fix this table. Minio is missing because they do not provide a public S3 compatibility list. +Feel free to open a PR to suggest fixes this table. Minio is missing because they do not provide a public S3 compatibility list. ## Update history - - 2022-02-07 - First version of this page - - 2022-05-25 - Many Ceph S3 endpoints are not documented but implemented. Following a notification from the Ceph community, we added them. +- 2022-02-07 - First version of this page +- 2022-05-25 - Many Ceph S3 endpoints are not documented but implemented. Following a notification from the Ceph community, we added them. -## Endpoint implementation -All APIs that are missing on Garage will return a 501 Not Implemented. -Some `x-amz-` headers are not implemented. -### Features +## High-level features | Feature | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | |------------------------------|----------------------------------|-----------------|---------------|---------|-----| @@ -30,34 +34,46 @@ Some `x-amz-` headers are not implemented. | [URL vhost-style](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html#virtual-hosted-style-access) URL (eg. `bucket.host.tld/key`) | ✅ Implemented | ❌| ✅| ✅ | ✅ | | [Presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html) | ✅ Implemented | ❌| ✅ | ✅ | ✅(❓) | -*Note:* OpenIO does not says if it supports presigned URLs. Because it is part of signature v4 and they claim they support it without additional precisions, we suppose that OpenIO supports presigned URLs. +*Note:* OpenIO does not says if it supports presigned URLs. Because it is part +of signature v4 and they claim they support it without additional precisions, +we suppose that OpenIO supports presigned URLs. + + +## Endpoint implementation + +All endpoints that are missing on Garage will return a 501 Not Implemented. +Some `x-amz-` headers are not implemented. ### Core endoints -| Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | +| Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | |------------------------------|----------------------------------|-----------------|---------------|---------|-----| | [CreateBucket](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateBucket.html) | ✅ Implemented | ✅ | ✅ | ✅ | ✅ | | [DeleteBucket](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucket.html) | ✅ Implemented | ✅ | ✅ | ✅ | ✅ | | [GetBucketLocation](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketLocation.html) | ✅ Implemented | ✅ | ✅ | ❌ | ✅ | | [HeadBucket](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadBucket.html) | ✅ Implemented | ✅ | ✅ | ✅ | ✅ | | [ListBuckets](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListBuckets.html) | ✅ Implemented | ❌| ✅ | ✅ | ✅ | -| [HeadObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html) | ✅ Implemented | ✅ | ✅ | ✅ | ✅ | -| [CopyObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CopyObject.html) | ✅ Implemented | ✅ | ✅ | ✅ | ✅ | +| [HeadObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html) | ✅ Implemented | ✅ | ✅ | ✅ | ✅ | +| [CopyObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CopyObject.html) | ✅ Implemented | ✅ | ✅ | ✅ | ✅ | | [DeleteObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html) | ✅ Implemented | ✅ | ✅ | ✅ | ✅ | | [DeleteObjects](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html) | ✅ Implemented | ✅ | ✅ | ✅ | ✅ | | [GetObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html) | ✅ Implemented | ✅ | ✅ | ✅ | ✅ | | [ListObjects](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjects.html) | ✅ Implemented (see details below) | ✅ | ✅ | ✅ | ❌| | [ListObjectsV2](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html) | ✅ Implemented | ❌| ✅ | ❌| ✅ | -| [PostObject](https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPOST.html) (compatibility API) | ❌ Missing | ❌| ✅ | ❌| ❌| +| [PostObject](https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPOST.html) | ✅ Implemented | ❌| ✅ | ❌| ❌| | [PutObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html) | ✅ Implemented | ✅ | ✅ | ✅ | ✅ | -**ListObjects:** Implemented, but there isn't a very good specification of what `encoding-type=url` covers so there might be some encoding bugs. In our implementation the url-encoded fields are in the same in ListObjects as they are in ListObjectsV2. +**ListObjects:** Implemented, but there isn't a very good specification of what +`encoding-type=url` covers so there might be some encoding bugs. In our +implementation the url-encoded fields are in the same in ListObjects as they +are in ListObjectsV2. -*Note: Ceph API documentation is incomplete and miss at least HeadBucket and UploadPartCopy, but these endpoints are documented in [Red Hat Ceph Storage - Chapter 2. Ceph Object Gateway and the S3 API](https://access.redhat.com/documentation/en-us/red_hat_ceph_storage/4/html/developer_guide/ceph-object-gateway-and-the-s3-api)* +*Note: Ceph API documentation is incomplete and lacks at least HeadBucket and UploadPartCopy, +but these endpoints are documented in [Red Hat Ceph Storage - Chapter 2. Ceph Object Gateway and the S3 API](https://access.redhat.com/documentation/en-us/red_hat_ceph_storage/4/html/developer_guide/ceph-object-gateway-and-the-s3-api)* ### Multipart Upload endpoints -| Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | +| Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | |------------------------------|----------------------------------|-----------------|---------------|---------|-----| | [AbortMultipartUpload](https://docs.aws.amazon.com/AmazonS3/latest/API/API_AbortMultipartUpload.html) | ✅ Implemented | ✅ | ✅ | ✅ | ✅ | | [CompleteMultipartUpload](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CompleteMultipartUpload.html) | ✅ Implemented (see details below) | ✅ | ✅ | ✅ | ✅ | @@ -72,7 +88,7 @@ For more information, please refer to our [issue tracker](https://git.deuxfleurs ### Website endpoints -| Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | +| Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | |------------------------------|----------------------------------|-----------------|---------------|---------|-----| | [DeleteBucketWebsite](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketWebsite.html) | ✅ Implemented | ❌| ❌| ❌| ❌| | [GetBucketWebsite](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketWebsite.html) | ✅ Implemented | ❌ | ❌| ❌| ❌| @@ -83,7 +99,7 @@ For more information, please refer to our [issue tracker](https://git.deuxfleurs **PutBucketWebsite:** Implemented, but only stores the index document suffix and the error document path. Redirects are not supported. -*Note: Ceph radosgw has some support for static websites but it is different from Amazon one plus it does not implement its configuration endpoints.* +*Note: Ceph radosgw has some support for static websites but it is different from the Amazon one. It also does not implement its configuration endpoints.* ### ACL, Policies endpoints @@ -91,7 +107,7 @@ Amazon has 2 access control mechanisms in S3: ACL (legacy) and policies (new one Garage implements none of them, and has its own system instead, built around a per-access-key-per-bucket logic. See Garage CLI reference manual to learn how to use Garage's permission system. -| Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | +| Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | |------------------------------|----------------------------------|-----------------|---------------|---------|-----| | [DeleteBucketPolicy](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketPolicy.html) | ❌ Missing | ❌| ✅ | ✅ | ❌| | [GetBucketPolicy](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketPolicy.html) | ❌ Missing | ❌| ✅ | ⚠ | ❌| @@ -106,10 +122,10 @@ See Garage CLI reference manual to learn how to use Garage's permission system. ### Versioning, Lifecycle endpoints -Garage does not support (yet) object versioning. +Garage does not (yet) support object versioning. If you need this feature, please [share your use case in our dedicated issue](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/166). -| Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | +| Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | |------------------------------|----------------------------------|-----------------|---------------|---------|-----| | [DeleteBucketLifecycle](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketLifecycle.html) | ❌ Missing | ❌| ✅| ❌| ✅| | [GetBucketLifecycleConfiguration](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketLifecycleConfiguration.html) | ❌ Missing | ❌| ✅ | ❌| ✅| @@ -125,19 +141,22 @@ If you need this feature, please [share your use case in our dedicated issue](ht Please open an issue if you have a use case for replication. -| Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | +| Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | |------------------------------|----------------------------------|-----------------|---------------|---------|-----| | [DeleteBucketReplication](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketReplication.html) | ❌ Missing | ❌| ✅ | ❌| ❌| | [GetBucketReplication](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketReplication.html) | ❌ Missing | ❌| ✅ | ❌| ❌| | [PutBucketReplication](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketReplication.html) | ❌ Missing | ❌| ⚠ | ❌| ❌| -*Note: Ceph documentation briefly says that Ceph supports [replication though the S3 API](https://docs.ceph.com/en/latest/radosgw/multisite-sync-policy/#s3-replication-api) but with some limitations. Additionaly, replication endpoints are not documented in the S3 compatibility page so I don't know what kind of support we can expect.* +*Note: Ceph documentation briefly says that Ceph supports +[replication though the S3 API](https://docs.ceph.com/en/latest/radosgw/multisite-sync-policy/#s3-replication-api) +but with some limitations. +Additionaly, replication endpoints are not documented in the S3 compatibility page so I don't know what kind of support we can expect.* ### Locking objects Amazon defines a concept of [object locking](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html) that can be achieved either through a Retention period or a Legal hold. -| Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | +| Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | |------------------------------|----------------------------------|-----------------|---------------|---------|-----| | [GetObjectLegalHold](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectLegalHold.html) | ❌ Missing | ❌| ✅ | ❌| ❌| | [PutObjectLegalHold](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectLegalHold.html) | ❌ Missing | ❌| ✅ | ❌| ❌| @@ -151,7 +170,7 @@ Amazon defines a concept of [object locking](https://docs.aws.amazon.com/AmazonS We think that you can either encrypt your server partition or do client-side encryption, so we did not implement server-side encryption for Garage. Please open an issue if you have a use case. -| Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | +| Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | |------------------------------|----------------------------------|-----------------|---------------|---------|-----| | [DeleteBucketEncryption](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketEncryption.html) | ❌ Missing | ❌| ✅ | ❌| ❌| | [GetBucketEncryption](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketEncryption.html) | ❌ Missing | ❌| ✅ | ❌| ❌| @@ -159,7 +178,7 @@ Please open an issue if you have a use case. ### Misc endpoints -| Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | +| Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | |------------------------------|----------------------------------|-----------------|---------------|---------|-----| | [GetBucketNotificationConfiguration](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketNotificationConfiguration.html) | ❌ Missing | ❌| ✅ | ❌| ❌| | [PutBucketNotificationConfiguration](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketNotificationConfiguration.html) | ❌ Missing | ❌| ✅ | ❌| ❌| @@ -176,7 +195,7 @@ Please open an issue if you have a use case.
Display Amazon specifc endpoints -| Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | +| Endpoint | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) | |------------------------------|----------------------------------|-----------------|---------------|---------|-----| | [DeleteBucketAnalyticsConfiguration](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketAnalyticsConfiguration.html) | ❌ Missing | ❌| ❌| ❌| ❌| | [DeleteBucketIntelligentTieringConfiguration](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketIntelligentTieringConfiguration.html) | ❌ Missing | ❌| ❌| ❌| ❌| From ff06d3f0829464863e64ed55471f2caa13bed191 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 25 May 2022 17:05:56 +0200 Subject: [PATCH 015/149] Fix Content-Type headers for {admin,k2v} errors and admin responses Fix #315 --- src/api/admin/bucket.rs | 13 +++---------- src/api/admin/cluster.rs | 19 +++++-------------- src/api/admin/error.rs | 5 +++-- src/api/admin/key.rs | 14 +++----------- src/api/generic_server.rs | 4 +--- src/api/helpers.rs | 10 +++++++++- src/api/k2v/error.rs | 5 +++-- src/api/s3/error.rs | 3 +++ 8 files changed, 30 insertions(+), 43 deletions(-) diff --git a/src/api/admin/bucket.rs b/src/api/admin/bucket.rs index 849d28ac..7f9a813f 100644 --- a/src/api/admin/bucket.rs +++ b/src/api/admin/bucket.rs @@ -6,7 +6,6 @@ use serde::{Deserialize, Serialize}; use garage_util::crdt::*; use garage_util::data::*; -use garage_util::error::Error as GarageError; use garage_util::time::*; use garage_table::*; @@ -19,7 +18,7 @@ use garage_model::permission::*; use crate::admin::error::*; use crate::admin::key::ApiBucketKeyPerm; use crate::common_error::CommonError; -use crate::helpers::parse_json_body; +use crate::helpers::{json_ok_response, parse_json_body}; pub async fn handle_list_buckets(garage: &Arc) -> Result, Error> { let buckets = garage @@ -60,10 +59,7 @@ pub async fn handle_list_buckets(garage: &Arc) -> Result, }) .collect::>(); - let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?; - Ok(Response::builder() - .status(StatusCode::OK) - .body(Body::from(resp_json))?) + Ok(json_ok_response(&res)?) } #[derive(Serialize)] @@ -197,10 +193,7 @@ async fn bucket_info_results( .collect::>(), }; - let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?; - Ok(Response::builder() - .status(StatusCode::OK) - .body(Body::from(resp_json))?) + Ok(json_ok_response(&res)?) } #[derive(Serialize)] diff --git a/src/api/admin/cluster.rs b/src/api/admin/cluster.rs index 3401be42..6d01317d 100644 --- a/src/api/admin/cluster.rs +++ b/src/api/admin/cluster.rs @@ -7,14 +7,13 @@ use serde::{Deserialize, Serialize}; use garage_util::crdt::*; use garage_util::data::*; -use garage_util::error::Error as GarageError; use garage_rpc::layout::*; use garage_model::garage::Garage; use crate::admin::error::*; -use crate::helpers::parse_json_body; +use crate::helpers::{json_ok_response, parse_json_body}; pub async fn handle_get_cluster_status(garage: &Arc) -> Result, Error> { let res = GetClusterStatusResponse { @@ -39,10 +38,7 @@ pub async fn handle_get_cluster_status(garage: &Arc) -> Result>(); - let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?; - Ok(Response::builder() - .status(StatusCode::OK) - .body(Body::from(resp_json))?) + Ok(json_ok_response(&res)?) } pub async fn handle_get_cluster_layout(garage: &Arc) -> Result, Error> { let res = get_cluster_layout(garage); - let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?; - Ok(Response::builder() - .status(StatusCode::OK) - .body(Body::from(resp_json))?) + + Ok(json_ok_response(&res)?) } fn get_cluster_layout(garage: &Arc) -> GetClusterLayoutResponse { diff --git a/src/api/admin/error.rs b/src/api/admin/error.rs index c4613cb3..ed1a07bd 100644 --- a/src/api/admin/error.rs +++ b/src/api/admin/error.rs @@ -72,8 +72,9 @@ impl ApiError for Error { } } - fn add_http_headers(&self, _header_map: &mut HeaderMap) { - // nothing + fn add_http_headers(&self, header_map: &mut HeaderMap) { + use hyper::header; + header_map.append(header::CONTENT_TYPE, "application/json".parse().unwrap()); } fn http_body(&self, garage_region: &str, path: &str) -> Body { diff --git a/src/api/admin/key.rs b/src/api/admin/key.rs index f30b5dbb..2bbabb7b 100644 --- a/src/api/admin/key.rs +++ b/src/api/admin/key.rs @@ -4,15 +4,13 @@ use std::sync::Arc; use hyper::{Body, Request, Response, StatusCode}; use serde::{Deserialize, Serialize}; -use garage_util::error::Error as GarageError; - use garage_table::*; use garage_model::garage::Garage; use garage_model::key_table::*; use crate::admin::error::*; -use crate::helpers::parse_json_body; +use crate::helpers::{json_ok_response, parse_json_body}; pub async fn handle_list_keys(garage: &Arc) -> Result, Error> { let res = garage @@ -32,10 +30,7 @@ pub async fn handle_list_keys(garage: &Arc) -> Result, Er }) .collect::>(); - let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?; - Ok(Response::builder() - .status(StatusCode::OK) - .body(Body::from(resp_json))?) + Ok(json_ok_response(&res)?) } #[derive(Serialize)] @@ -221,10 +216,7 @@ async fn key_info_results(garage: &Arc, key: Key) -> Result>(), }; - let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?; - Ok(Response::builder() - .status(StatusCode::OK) - .body(Body::from(resp_json))?) + Ok(json_ok_response(&res)?) } #[derive(Serialize)] diff --git a/src/api/generic_server.rs b/src/api/generic_server.rs index 77278908..a48be1bc 100644 --- a/src/api/generic_server.rs +++ b/src/api/generic_server.rs @@ -150,9 +150,7 @@ impl ApiServer { } Err(e) => { let body: Body = e.http_body(&self.region, uri.path()); - let mut http_error_builder = Response::builder() - .status(e.http_status_code()) - .header("Content-Type", "application/xml"); + let mut http_error_builder = Response::builder().status(e.http_status_code()); if let Some(header_map) = http_error_builder.headers_mut() { e.add_http_headers(header_map) diff --git a/src/api/helpers.rs b/src/api/helpers.rs index 9fb12dbe..642dbc42 100644 --- a/src/api/helpers.rs +++ b/src/api/helpers.rs @@ -1,4 +1,4 @@ -use hyper::{Body, Request}; +use hyper::{Body, Request, Response}; use idna::domain_to_unicode; use serde::{Deserialize, Serialize}; @@ -144,6 +144,14 @@ pub async fn parse_json_body Deserialize<'de>>(req: Request) - Ok(resp) } +pub fn json_ok_response(res: &T) -> Result, Error> { + let resp_json = serde_json::to_string_pretty(res).map_err(garage_util::error::Error::from)?; + Ok(Response::builder() + .status(hyper::StatusCode::OK) + .header(http::header::CONTENT_TYPE, "application/json") + .body(Body::from(resp_json))?) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/api/k2v/error.rs b/src/api/k2v/error.rs index 4c55d8b5..42491466 100644 --- a/src/api/k2v/error.rs +++ b/src/api/k2v/error.rs @@ -110,8 +110,9 @@ impl ApiError for Error { } } - fn add_http_headers(&self, _header_map: &mut HeaderMap) { - // nothing + fn add_http_headers(&self, header_map: &mut HeaderMap) { + use hyper::header; + header_map.append(header::CONTENT_TYPE, "application/json".parse().unwrap()); } fn http_body(&self, garage_region: &str, path: &str) -> Body { diff --git a/src/api/s3/error.rs b/src/api/s3/error.rs index ac632540..67009d63 100644 --- a/src/api/s3/error.rs +++ b/src/api/s3/error.rs @@ -172,6 +172,9 @@ impl ApiError for Error { fn add_http_headers(&self, header_map: &mut HeaderMap) { use hyper::header; + + header_map.append(header::CONTENT_TYPE, "application/xml".parse().unwrap()); + #[allow(clippy::single_match)] match self { Error::InvalidRange((_, len)) => { From b54a938724e5551f6436f551cafec3d1324a6260 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 30 May 2022 11:11:35 +0200 Subject: [PATCH 016/149] Fix garage_version() now that GIT_VERSION is read in crate garage_rpc --- default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.nix b/default.nix index 21a413b2..de996ac1 100644 --- a/default.nix +++ b/default.nix @@ -69,7 +69,7 @@ in let we ask the user (the CI often) to pass the value to Nix. */ (pkgs.rustBuilder.rustLib.makeOverride { - name = "garage"; + name = "garage_rpc"; overrideAttrs = drv: /* [1] */ { hardeningDisable = [ "pie" ]; } // From a1abed0378f14792bfc45f98a6abcf91b31cc3fe Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Thu, 2 Jun 2022 12:50:11 +0200 Subject: [PATCH 017/149] Remove useless MC_REGION env variable --- doc/book/quick-start/_index.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/doc/book/quick-start/_index.md b/doc/book/quick-start/_index.md index 025747bc..5d7df48e 100644 --- a/doc/book/quick-start/_index.md +++ b/doc/book/quick-start/_index.md @@ -249,16 +249,6 @@ mc alias set \ --api S3v4 ``` -You must also add an environment variable to your configuration to -inform MinIO of our region (`garage` by default, corresponding to the `s3_region` parameter -in the configuration file). -The best way is to add the following snippet to your `$HOME/.bash_profile` -or `$HOME/.bashrc` file: - -```bash -export MC_REGION=garage -``` - ### Use `mc` You can not list buckets from `mc` currently. From 7d3b5585f1662dbff85b189d4d3ad7e4fc4c96ea Mon Sep 17 00:00:00 2001 From: Simon C Date: Tue, 7 Jun 2022 09:38:59 +0200 Subject: [PATCH 018/149] docs: Add link to facilitate navigation in the documentation --- doc/book/connect/websites.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/book/connect/websites.md b/doc/book/connect/websites.md index da3dac90..7b49fcad 100644 --- a/doc/book/connect/websites.md +++ b/doc/book/connect/websites.md @@ -3,7 +3,7 @@ title = "Websites (Hugo, Jekyll, Publii...)" weight = 10 +++ -Garage is also suitable to host static websites. +Garage is also suitable [to host static websites](@/documentation/cookbook/exposing-websites.md). While they can be deployed with traditional CLI tools, some static website generators have integrated options to ease your workflow. | Name | Status | Note | From 4b8f48f3c535949fe4550aade6df83b9dca989e7 Mon Sep 17 00:00:00 2001 From: Simon C Date: Tue, 7 Jun 2022 09:44:03 +0200 Subject: [PATCH 019/149] docs: Fix title level --- doc/book/cookbook/reverse-proxy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/book/cookbook/reverse-proxy.md b/doc/book/cookbook/reverse-proxy.md index 61bc7933..81882451 100644 --- a/doc/book/cookbook/reverse-proxy.md +++ b/doc/book/cookbook/reverse-proxy.md @@ -100,7 +100,7 @@ server { } ``` -## Exposing the web endpoint +### Exposing the web endpoint To better understand the logic involved, you can refer to the [Exposing buckets as websites](/cookbook/exposing_websites.html) section. Otherwise, the configuration is very similar to the S3 endpoint. From 7eed3ceda9cf964e3435f22fc1852e27f4f5a8ae Mon Sep 17 00:00:00 2001 From: Simon C Date: Tue, 7 Jun 2022 11:21:48 +0200 Subject: [PATCH 020/149] docs: Add Trafik reverse proxy documentation --- doc/book/cookbook/reverse-proxy.md | 141 ++++++++++++++++++++++++++++- 1 file changed, 139 insertions(+), 2 deletions(-) diff --git a/doc/book/cookbook/reverse-proxy.md b/doc/book/cookbook/reverse-proxy.md index 81882451..27add5bf 100644 --- a/doc/book/cookbook/reverse-proxy.md +++ b/doc/book/cookbook/reverse-proxy.md @@ -140,6 +140,143 @@ server { @TODO -## Traefik +## Traefik v2 -@TODO +We will see in this part how to set up a reverse proxy with [Traefik](https://docs.traefik.io/). + +Here is [a basic configuration file](https://doc.traefik.io/traefik/https/acme/#configuration-examples): + +```toml +[entryPoints] + [entryPoints.web] + address = ":80" + + [entryPoints.websecure] + address = ":443" + +[certificatesResolvers.myresolver.acme] + email = "your-email@example.com" + storage = "acme.json" + [certificatesResolvers.myresolver.acme.httpChallenge] + # used during the challenge + entryPoint = "web" +``` + +### Add Garage service + +To add Garage on Traefik you should declare a new service using its IP address (or hostname) and port: + +```toml +[http.services] + [http.services.my_garage_service.loadBalancer] + [[http.services.my_garage_service.loadBalancer.servers]] + url = "http://xxx.xxx.xxx.xxx" + port = 3900 +``` + +It's possible to declare multiple Garage servers as back-ends: + +```toml +[http.services] + [[http.services.my_garage_service.loadBalancer.servers]] + url = "http://xxx.xxx.xxx.xxx" + port = 3900 + [[http.services.my_garage_service.loadBalancer.servers]] + url = "http://yyy.yyy.yyy.yyy" + port = 3900 + [[http.services.my_garage_service.loadBalancer.servers]] + url = "http://zzz.zzz.zzz.zzz" + port = 3900 +``` + +Traefik can remove unhealthy servers automatically with [a health check configuration](https://doc.traefik.io/traefik/routing/services/#health-check): + +``` +[http.services] + [http.services.my_garage_service.loadBalancer] + [http.services.my_garage_service.loadBalancer.healthCheck] + path = "/" + interval = "60s" + timeout = "5s" +``` + +### Adding a website + +To add a new website, add the following declaration to your Traefik configuration file: + +```toml +[http.routers] + [http.routers.my_website] + rule = "Host(`yoururl.example.org`)" + service = "my_garage_service" + entryPoints = ["web"] +``` + +Enable HTTPS access to your website with the following configuration section ([documentation](https://doc.traefik.io/traefik/https/overview/)): + +```toml +... + entryPoints = ["websecure"] + [http.routers.my_website.tls] + certResolver = "myresolver" +... +``` + +### Adding gzip compression + +Add the following configuration section [to compress response](https://doc.traefik.io/traefik/middlewares/http/compress/) using [gzip](https://developer.mozilla.org/en-US/docs/Glossary/GZip_compression) before sending them to the client: + +```toml +[http.routers] + [http.routers.my_website] + ... + middlewares = ["gzip_compress"] + ... +[http.middlewares] + [http.middlewares.gzip_compress.compress] +``` + +### Add caching response + +Traefik's caching middleware is only available on [entreprise version](https://doc.traefik.io/traefik-enterprise/middlewares/http-cache/), however the freely-available [Souin plugin](https://github.com/darkweak/souin#tr%C3%A6fik-container) can also do the job. (section to be completed) + +### Complete example + +```toml +[entryPoints] + [entryPoints.web] + address = ":80" + + [entryPoints.websecure] + address = ":443" + +[certificatesResolvers.myresolver.acme] + email = "your-email@example.com" + storage = "acme.json" + [certificatesResolvers.myresolver.acme.httpChallenge] + # used during the challenge + entryPoint = "web" + +[http.routers] + [http.routers.my_website] + rule = "Host(`yoururl.example.org`)" + service = "my_garage_service" + middlewares = ["gzip_compress"] + entryPoints = ["websecure"] + +[http.services] + [http.services.my_garage_service.loadBalancer] + [http.services.my_garage_service.loadBalancer.healthCheck] + path = "/" + interval = "60s" + timeout = "5s" + [[http.services.my_garage_service.loadBalancer.servers]] + url = "http://xxx.xxx.xxx.xxx" + [[http.services.my_garage_service.loadBalancer.servers]] + url = "http://yyy.yyy.yyy.yyy" + [[http.services.my_garage_service.loadBalancer.servers]] + url = "http://zzz.zzz.zzz.zzz" + +[http.middlewares] + [http.middlewares.gzip_compress.compress] +``` From b44d3fc796484a50cd6854f20c9b46e5fddedc9d Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 8 Jun 2022 10:01:44 +0200 Subject: [PATCH 021/149] Abstract database behind generic interface and implement alternative drivers (#322) - [x] Design interface - [x] Implement Sled backend - [x] Re-implement the SledCountedTree hack ~~on Sled backend~~ on all backends (i.e. over the abstraction) - [x] Convert Garage code to use generic interface - [x] Proof-read converted Garage code - [ ] Test everything well - [x] Implement sqlite backend - [x] Implement LMDB backend - [ ] (Implement Persy backend?) - [ ] (Implement other backends? (like RocksDB, ...)) - [x] Implement backend choice in config file and garage server module - [x] Add CLI for converting between DB formats - Exploit the new interface to put more things in transactions - [x] `.updated()` trigger on Garage tables Fix #284 **Bugs** - [x] When exporting sqlite, trees iterate empty?? - [x] LMDB doesn't work **Known issues for various back-ends** - Sled: - Eats all my RAM and also all my disk space - `.len()` has to traverse the whole table - Is actually quite slow on some operations - And is actually pretty bad code... - Sqlite: - Requires a lock to be taken on all operations. The lock is also taken when iterating on a table with `.iter()`, and the lock isn't released until the iterator is dropped. This means that we must be VERY carefull to not do anything else inside a `.iter()` loop or else we will have a deadlock! Most such cases have been eliminated from the Garage codebase, but there might still be some that remain. If your Garage-over-Sqlite seems to hang/freeze, this is the reason. - (adapter uses a bunch of unsafe code) - Heed (LMDB): - Not suited for 32-bit machines as it has to map the whole DB in memory. - (adpater uses a tiny bit of unsafe code) **My recommendation:** avoid 32-bit machines and use LMDB as much as possible. **Converting databases** is actually quite easy. For example from Sled to LMDB: ```bash cd src/db cargo run --features cli --bin convert -- -i path/to/garage/meta/db -a sled -o path/to/garage/meta/db.lmdb -b lmdb ``` Then, just add this to your `config.toml`: ```toml db_engine = "lmdb" ``` Co-authored-by: Alex Auvolat Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/322 Co-authored-by: Alex Co-committed-by: Alex --- Cargo.lock | 257 ++++++++++-- Cargo.nix | 694 +++++++++++++++++++++++--------- Cargo.toml | 1 + src/api/admin/cluster.rs | 2 + src/block/Cargo.toml | 3 +- src/block/manager.rs | 127 ++++-- src/block/metrics.rs | 4 +- src/block/rc.rs | 49 ++- src/db/Cargo.toml | 36 ++ src/db/bin/convert.rs | 76 ++++ src/db/counted_tree_hack.rs | 127 ++++++ src/db/lib.rs | 400 ++++++++++++++++++ src/db/lmdb_adapter.rs | 329 +++++++++++++++ src/db/sled_adapter.rs | 260 ++++++++++++ src/db/sqlite_adapter.rs | 500 +++++++++++++++++++++++ src/db/test.rs | 106 +++++ src/garage/Cargo.toml | 3 +- src/garage/admin.rs | 45 ++- src/garage/repair.rs | 50 ++- src/garage/server.rs | 54 ++- src/garage/tests/bucket.rs | 8 +- src/model/Cargo.toml | 3 +- src/model/garage.rs | 8 +- src/model/index_counter.rs | 60 ++- src/model/k2v/item_table.rs | 24 +- src/model/migrate.rs | 6 +- src/model/s3/block_ref_table.rs | 21 +- src/model/s3/object_table.rs | 12 +- src/model/s3/version_table.rs | 13 +- src/table/Cargo.toml | 3 +- src/table/data.rs | 115 +++--- src/table/gc.rs | 41 +- src/table/merkle.rs | 101 ++--- src/table/metrics.rs | 21 +- src/table/schema.rs | 19 +- src/table/sync.rs | 16 +- src/table/table.rs | 4 +- src/util/Cargo.toml | 4 +- src/util/config.rs | 11 +- src/util/error.rs | 12 +- src/util/lib.rs | 2 +- src/util/sled_counter.rs | 100 ----- 42 files changed, 3086 insertions(+), 641 deletions(-) create mode 100644 src/db/Cargo.toml create mode 100644 src/db/bin/convert.rs create mode 100644 src/db/counted_tree_hack.rs create mode 100644 src/db/lib.rs create mode 100644 src/db/lmdb_adapter.rs create mode 100644 src/db/sled_adapter.rs create mode 100644 src/db/sqlite_adapter.rs create mode 100644 src/db/test.rs delete mode 100644 src/util/sled_counter.rs diff --git a/Cargo.lock b/Cargo.lock index 630642ff..11aa070d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -301,6 +312,15 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[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" @@ -333,6 +353,12 @@ version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" +[[package]] +name = "bytemuck" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc" + [[package]] name = "byteorder" version = "1.4.3" @@ -370,6 +396,12 @@ dependencies = [ "jobserver", ] +[[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" @@ -486,7 +518,7 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -495,8 +527,8 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" dependencies = [ - "cfg-if", - "crossbeam-utils", + "cfg-if 1.0.0", + "crossbeam-utils 0.8.8", ] [[package]] @@ -506,20 +538,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" dependencies = [ "autocfg", - "cfg-if", - "crossbeam-utils", + "cfg-if 1.0.0", + "crossbeam-utils 0.8.8", "lazy_static", "memoffset", "scopeguard", ] +[[package]] +name = "crossbeam-queue" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b" +dependencies = [ + "crossbeam-utils 0.6.6", +] + +[[package]] +name = "crossbeam-utils" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6" +dependencies = [ + "cfg-if 0.1.10", + "lazy_static", +] + [[package]] name = "crossbeam-utils" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "lazy_static", ] @@ -603,7 +654,7 @@ version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "num_cpus", ] @@ -633,7 +684,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "dirs-sys-next", ] @@ -672,7 +723,7 @@ version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -716,6 +767,18 @@ dependencies = [ "synstructure", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "1.7.0" @@ -889,6 +952,7 @@ dependencies = [ "futures", "futures-util", "garage_api", + "garage_db", "garage_model 0.7.0", "garage_rpc 0.7.0", "garage_table 0.7.0", @@ -911,7 +975,6 @@ dependencies = [ "serde_bytes", "serde_json", "sha2", - "sled", "static_init", "structopt", "tokio", @@ -972,6 +1035,7 @@ dependencies = [ "bytes 1.1.0", "futures", "futures-util", + "garage_db", "garage_rpc 0.7.0", "garage_table 0.7.0", "garage_util 0.7.0", @@ -981,12 +1045,26 @@ dependencies = [ "rmp-serde 0.15.5", "serde", "serde_bytes", - "sled", "tokio", "tracing", "zstd", ] +[[package]] +name = "garage_db" +version = "0.8.0" +dependencies = [ + "clap 3.1.18", + "err-derive 0.3.1", + "heed", + "hexdump", + "log", + "mktemp", + "pretty_env_logger", + "rusqlite", + "sled", +] + [[package]] name = "garage_model" version = "0.5.1" @@ -1024,6 +1102,7 @@ dependencies = [ "futures", "futures-util", "garage_block", + "garage_db", "garage_model 0.5.1", "garage_rpc 0.7.0", "garage_table 0.7.0", @@ -1035,7 +1114,6 @@ dependencies = [ "rmp-serde 0.15.5", "serde", "serde_bytes", - "sled", "tokio", "tracing", "zstd", @@ -1130,6 +1208,7 @@ dependencies = [ "bytes 1.1.0", "futures", "futures-util", + "garage_db", "garage_rpc 0.7.0", "garage_util 0.7.0", "hexdump", @@ -1138,7 +1217,6 @@ dependencies = [ "rmp-serde 0.15.5", "serde", "serde_bytes", - "sled", "tokio", "tracing", ] @@ -1177,6 +1255,7 @@ dependencies = [ "chrono", "err-derive 0.3.1", "futures", + "garage_db", "hex", "http", "hyper", @@ -1187,7 +1266,6 @@ dependencies = [ "serde", "serde_json", "sha2", - "sled", "tokio", "toml", "tracing", @@ -1237,7 +1315,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "wasi 0.10.0+wasi-snapshot-preview1", ] @@ -1288,6 +1366,18 @@ name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown", +] [[package]] name = "heck" @@ -1304,6 +1394,45 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +[[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" @@ -1503,7 +1632,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -1758,12 +1887,34 @@ dependencies = [ "walkdir", ] +[[package]] +name = "libsqlite3-sys" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linked-hash-map" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" +[[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.6" @@ -1779,7 +1930,7 @@ version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -1855,6 +2006,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "mktemp" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "975de676448231fcde04b9149d2543077e166b78fc29eae5aa219e7928410da2" +dependencies = [ + "uuid", +] + [[package]] name = "multer" version = "2.0.2" @@ -1928,7 +2088,7 @@ dependencies = [ "arc-swap", "async-trait", "bytes 0.6.0", - "cfg-if", + "cfg-if 1.0.0", "err-derive 0.2.4", "futures", "hex", @@ -2021,7 +2181,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" dependencies = [ "bitflags", - "cfg-if", + "cfg-if 1.0.0", "foreign-types", "libc", "once_cell", @@ -2134,6 +2294,16 @@ version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "029d8d0b2f198229de29dca79676f2738ff952edf3fde542eb8bf94d8c21b435" +[[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 = "parking_lot" version = "0.11.2" @@ -2161,7 +2331,7 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "instant", "libc", "redox_syscall", @@ -2175,7 +2345,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28141e0cc4143da2443301914478dc976a61ffdb3f043058310c70df2fed8954" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "redox_syscall", "smallvec", @@ -2357,7 +2527,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f64969ffd5dd8f39bd57a68ac53c163a095ed9d0fb707146da1b27025a3504" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "fnv", "lazy_static", "memchr", @@ -2679,6 +2849,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "rusqlite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "memchr", + "smallvec", +] + [[package]] name = "rustc_version" version = "0.4.0" @@ -2894,7 +3079,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer", - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest", "opaque-debug", @@ -2929,7 +3114,7 @@ checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" dependencies = [ "crc32fast", "crossbeam-epoch", - "crossbeam-utils", + "crossbeam-utils 0.8.8", "fs2", "fxhash", "libc", @@ -3063,6 +3248,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "synchronoise" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d717ed0efc9d39ab3b642a096bc369a3e02a38a51c41845d7fe31bdad1d6eaeb" +dependencies = [ + "crossbeam-queue", +] + [[package]] name = "synstructure" version = "0.12.6" @@ -3081,7 +3275,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "fastrand", "libc", "redox_syscall", @@ -3380,7 +3574,7 @@ version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1bdf54a7c28a2bbf701e1d2233f6c77f473486b94bee4f9678da5a148dca7f" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "log", "pin-project-lite", "tracing-attributes", @@ -3489,6 +3683,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -3540,7 +3743,7 @@ version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "wasm-bindgen-macro", ] diff --git a/Cargo.nix b/Cargo.nix index d100e7bb..335651fc 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -4,6 +4,7 @@ args@{ release ? true, rootFeatures ? [ + "garage_db/default" "garage_util/default" "garage_rpc/default" "garage_table/default" @@ -45,6 +46,7 @@ in { cargo2nixVersion = "0.10.0"; workspace = { + garage_db = rustPackages.unknown.garage_db."0.8.0"; garage_util = rustPackages.unknown.garage_util."0.7.0"; garage_rpc = rustPackages.unknown.garage_rpc."0.7.0"; garage_table = rustPackages.unknown.garage_table."0.7.0"; @@ -55,6 +57,20 @@ in garage = rustPackages.unknown.garage."0.7.0"; k2v-client = rustPackages.unknown.k2v-client."0.1.0"; }; + "registry+https://github.com/rust-lang/crates.io-index".ahash."0.7.6" = overridableMkRustCrate (profileName: rec { + name = "ahash"; + version = "0.7.6"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"; }; + dependencies = { + ${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "windows" || hostPlatform.parsed.kernel.name == "darwin" || hostPlatform.parsed.kernel.name == "ios" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "solaris" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "fuchsia" || hostPlatform.parsed.kernel.name == "redox" || hostPlatform.parsed.kernel.name == "cloudabi" || hostPlatform.parsed.kernel.name == "haiku" || hostPlatform.parsed.kernel.name == "vxworks" || hostPlatform.parsed.kernel.name == "emscripten" || hostPlatform.parsed.kernel.name == "wasi" then "getrandom" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".getrandom."0.2.5" { inherit profileName; }; + ${ if !((hostPlatform.parsed.cpu.name == "armv6l" || hostPlatform.parsed.cpu.name == "armv7l") && hostPlatform.parsed.kernel.name == "none") then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + }; + buildDependencies = { + version_check = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".version_check."0.9.4" { profileName = "__noProfile"; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".aho-corasick."0.7.18" = overridableMkRustCrate (profileName: rec { name = "aho-corasick"; version = "0.7.18"; @@ -433,6 +449,16 @@ in ]; }); + "registry+https://github.com/rust-lang/crates.io-index".bincode."1.3.3" = overridableMkRustCrate (profileName: rec { + name = "bincode"; + version = "1.3.3"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"; }; + dependencies = { + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" = overridableMkRustCrate (profileName: rec { name = "bitflags"; version = "1.3.2"; @@ -479,6 +505,17 @@ in ]; }); + "registry+https://github.com/rust-lang/crates.io-index".bytemuck."1.9.1" = overridableMkRustCrate (profileName: rec { + name = "bytemuck"; + version = "1.9.1"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc"; }; + features = builtins.concatLists [ + [ "extern_crate_alloc" ] + [ "extern_crate_std" ] + ]; + }); + "registry+https://github.com/rust-lang/crates.io-index".byteorder."1.4.3" = overridableMkRustCrate (profileName: rec { name = "byteorder"; version = "1.4.3"; @@ -541,6 +578,13 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".cfg-if."0.1.10" = overridableMkRustCrate (profileName: rec { + name = "cfg-if"; + version = "0.1.10"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"; }; + }); + "registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" = overridableMkRustCrate (profileName: rec { name = "cfg-if"; version = "1.0.0"; @@ -561,22 +605,22 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"; }; features = builtins.concatLists [ - [ "clock" ] - [ "default" ] - [ "libc" ] - [ "oldtime" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "clock") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "libc") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "oldtime") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "serde") - [ "std" ] - [ "time" ] - [ "winapi" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "std") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "time") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "winapi") ]; dependencies = { - libc = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - num_integer = rustPackages."registry+https://github.com/rust-lang/crates.io-index".num-integer."0.1.44" { inherit profileName; }; - num_traits = rustPackages."registry+https://github.com/rust-lang/crates.io-index".num-traits."0.2.14" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "num_integer" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".num-integer."0.1.44" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "num_traits" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".num-traits."0.2.14" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; - time = rustPackages."registry+https://github.com/rust-lang/crates.io-index".time."0.1.44" { inherit profileName; }; - ${ if hostPlatform.isWindows then "winapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".winapi."0.3.9" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "time" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".time."0.1.44" { inherit profileName; }; + ${ if (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") && hostPlatform.isWindows then "winapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".winapi."0.3.9" { inherit profileName; }; }; }); @@ -688,7 +732,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.config == "aarch64-linux-android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -745,6 +789,32 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".crossbeam-queue."0.1.2" = overridableMkRustCrate (profileName: rec { + name = "crossbeam-queue"; + version = "0.1.2"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b"; }; + dependencies = { + crossbeam_utils = rustPackages."registry+https://github.com/rust-lang/crates.io-index".crossbeam-utils."0.6.6" { inherit profileName; }; + }; + }); + + "registry+https://github.com/rust-lang/crates.io-index".crossbeam-utils."0.6.6" = overridableMkRustCrate (profileName: rec { + name = "crossbeam-utils"; + version = "0.6.6"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6"; }; + features = builtins.concatLists [ + [ "default" ] + [ "lazy_static" ] + [ "std" ] + ]; + dependencies = { + cfg_if = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."0.1.10" { inherit profileName; }; + lazy_static = rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".crossbeam-utils."0.8.8" = overridableMkRustCrate (profileName: rec { name = "crossbeam-utils"; version = "0.8.8"; @@ -1018,6 +1088,24 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".fallible-iterator."0.2.0" = overridableMkRustCrate (profileName: rec { + name = "fallible-iterator"; + version = "0.2.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"; }; + features = builtins.concatLists [ + [ "default" ] + [ "std" ] + ]; + }); + + "registry+https://github.com/rust-lang/crates.io-index".fallible-streaming-iterator."0.1.9" = overridableMkRustCrate (profileName: rec { + name = "fallible-streaming-iterator"; + version = "0.1.9"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"; }; + }); + "registry+https://github.com/rust-lang/crates.io-index".fastrand."1.7.0" = overridableMkRustCrate (profileName: rec { name = "fastrand"; version = "1.7.0"; @@ -1214,32 +1302,32 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"; }; features = builtins.concatLists [ - [ "alloc" ] - [ "async-await" ] - [ "async-await-macro" ] - [ "channel" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "alloc") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "async-await") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "async-await-macro") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "channel") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "default") - [ "futures-channel" ] - [ "futures-io" ] - [ "futures-macro" ] - [ "futures-sink" ] - [ "io" ] - [ "memchr" ] - [ "sink" ] - [ "slab" ] - [ "std" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "futures-channel") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "futures-io") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "futures-macro") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "futures-sink") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "io") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "memchr") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "sink") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "slab") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "std") ]; dependencies = { - futures_channel = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-channel."0.3.21" { inherit profileName; }; - futures_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; - futures_io = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-io."0.3.21" { inherit profileName; }; - futures_macro = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-macro."0.3.21" { profileName = "__noProfile"; }; - futures_sink = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-sink."0.3.21" { inherit profileName; }; - futures_task = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-task."0.3.21" { inherit profileName; }; - memchr = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; }; - pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; - pin_utils = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-utils."0.1.0" { inherit profileName; }; - slab = rustPackages."registry+https://github.com/rust-lang/crates.io-index".slab."0.4.5" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_channel" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-channel."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_io" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-io."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_macro" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-macro."0.3.21" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_sink" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-sink."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_task" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-task."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "memchr" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "pin_project_lite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "pin_utils" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-utils."0.1.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "slab" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".slab."0.4.5" { inherit profileName; }; }; }); @@ -1268,6 +1356,7 @@ in futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; garage_api = rustPackages."unknown".garage_api."0.7.0" { inherit profileName; }; + garage_db = rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }; garage_model = rustPackages."unknown".garage_model."0.7.0" { inherit profileName; }; garage_rpc = rustPackages."unknown".garage_rpc."0.7.0" { inherit profileName; }; garage_table = rustPackages."unknown".garage_table."0.7.0" { inherit profileName; }; @@ -1285,7 +1374,6 @@ in rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; serde_bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; - sled = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; structopt = rustPackages."registry+https://github.com/rust-lang/crates.io-index".structopt."0.3.26" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; toml = rustPackages."registry+https://github.com/rust-lang/crates.io-index".toml."0.5.8" { inherit profileName; }; @@ -1366,6 +1454,7 @@ in bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; + garage_db = rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }; garage_rpc = rustPackages."unknown".garage_rpc."0.7.0" { inherit profileName; }; garage_table = rustPackages."unknown".garage_table."0.7.0" { inherit profileName; }; garage_util = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; @@ -1375,13 +1464,37 @@ in rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; serde_bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; - sled = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; tracing = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; zstd = rustPackages."registry+https://github.com/rust-lang/crates.io-index".zstd."0.9.2+zstd.1.5.1" { inherit profileName; }; }; }); + "unknown".garage_db."0.8.0" = overridableMkRustCrate (profileName: rec { + name = "garage_db"; + version = "0.8.0"; + registry = "unknown"; + src = fetchCrateLocal (workspaceSrc + "/src/db"); + features = builtins.concatLists [ + (lib.optional (rootFeatures' ? "garage_db") "clap") + (lib.optional (rootFeatures' ? "garage_db") "cli") + (lib.optional (rootFeatures' ? "garage_db") "pretty_env_logger") + ]; + dependencies = { + ${ if rootFeatures' ? "garage_db" then "clap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".clap."3.1.18" { inherit profileName; }; + err_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; + heed = rustPackages."registry+https://github.com/rust-lang/crates.io-index".heed."0.11.0" { inherit profileName; }; + hexdump = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hexdump."0.1.1" { inherit profileName; }; + log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; + ${ if rootFeatures' ? "garage_db" then "pretty_env_logger" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pretty_env_logger."0.4.0" { inherit profileName; }; + rusqlite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rusqlite."0.27.0" { inherit profileName; }; + sled = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; + }; + devDependencies = { + mktemp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".mktemp."0.4.1" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".garage_model."0.5.1" = overridableMkRustCrate (profileName: rec { name = "garage_model"; version = "0.5.1"; @@ -1425,6 +1538,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_block" else null } = rustPackages."unknown".garage_block."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_db" else null } = rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_model_050" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".garage_model."0.5.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_rpc" else null } = rustPackages."unknown".garage_rpc."0.7.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_table" else null } = rustPackages."unknown".garage_table."0.7.0" { inherit profileName; }; @@ -1436,7 +1550,6 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "serde_bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "sled" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "tracing" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "zstd" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".zstd."0.9.2+zstd.1.5.1" { inherit profileName; }; @@ -1546,6 +1659,7 @@ in bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; + garage_db = rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }; garage_rpc = rustPackages."unknown".garage_rpc."0.7.0" { inherit profileName; }; garage_util = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; hexdump = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hexdump."0.1.1" { inherit profileName; }; @@ -1554,7 +1668,6 @@ in rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; serde_bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; - sled = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; tracing = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; }; @@ -1596,25 +1709,25 @@ in (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_util") "k2v") ]; dependencies = { - blake2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".blake2."0.9.2" { inherit profileName; }; - chrono = rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.19" { inherit profileName; }; - err_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; - futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; - hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; - http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; - hyper = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; - netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; - opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; - rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; - rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; - serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; - sha2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sha2."0.9.9" { inherit profileName; }; - sled = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; - tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; - toml = rustPackages."registry+https://github.com/rust-lang/crates.io-index".toml."0.5.8" { inherit profileName; }; - tracing = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; - xxhash_rust = rustPackages."registry+https://github.com/rust-lang/crates.io-index".xxhash-rust."0.8.4" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "blake2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".blake2."0.9.2" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "chrono" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.19" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "err_derive" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "garage_db" else null } = rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "http" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "hyper" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "serde_json" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "sha2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sha2."0.9.9" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "toml" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".toml."0.5.8" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tracing" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "xxhash_rust" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".xxhash-rust."0.8.4" { inherit profileName; }; }; }); @@ -1668,7 +1781,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77"; }; features = builtins.concatLists [ - [ "std" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "std") ]; dependencies = { cfg_if = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }; @@ -1727,8 +1840,24 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"; }; features = builtins.concatLists [ - [ "raw" ] + [ "ahash" ] + [ "default" ] + [ "inline-more" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "raw") ]; + dependencies = { + ahash = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ahash."0.7.6" { inherit profileName; }; + }; + }); + + "registry+https://github.com/rust-lang/crates.io-index".hashlink."0.7.0" = overridableMkRustCrate (profileName: rec { + name = "hashlink"; + version = "0.7.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf"; }; + dependencies = { + hashbrown = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hashbrown."0.11.2" { inherit profileName; }; + }; }); "registry+https://github.com/rust-lang/crates.io-index".heck."0.3.3" = overridableMkRustCrate (profileName: rec { @@ -1751,6 +1880,64 @@ in ]; }); + "registry+https://github.com/rust-lang/crates.io-index".heed."0.11.0" = overridableMkRustCrate (profileName: rec { + name = "heed"; + version = "0.11.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "269c7486ed6def5d7b59a427cec3e87b4d4dd4381d01e21c8c9f2d3985688392"; }; + features = builtins.concatLists [ + [ "default" ] + [ "lmdb" ] + [ "lmdb-rkv-sys" ] + [ "serde" ] + [ "serde-bincode" ] + [ "serde-json" ] + ]; + dependencies = { + bytemuck = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytemuck."1.9.1" { inherit profileName; }; + byteorder = rustPackages."registry+https://github.com/rust-lang/crates.io-index".byteorder."1.4.3" { inherit profileName; }; + heed_traits = rustPackages."registry+https://github.com/rust-lang/crates.io-index".heed-traits."0.8.0" { inherit profileName; }; + heed_types = rustPackages."registry+https://github.com/rust-lang/crates.io-index".heed-types."0.8.0" { inherit profileName; }; + libc = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + lmdb_sys = rustPackages."registry+https://github.com/rust-lang/crates.io-index".lmdb-rkv-sys."0.11.2" { inherit profileName; }; + once_cell = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + page_size = rustPackages."registry+https://github.com/rust-lang/crates.io-index".page_size."0.4.2" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + synchronoise = rustPackages."registry+https://github.com/rust-lang/crates.io-index".synchronoise."1.0.0" { inherit profileName; }; + ${ if hostPlatform.isWindows then "url" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".url."2.2.2" { inherit profileName; }; + }; + }); + + "registry+https://github.com/rust-lang/crates.io-index".heed-traits."0.8.0" = overridableMkRustCrate (profileName: rec { + name = "heed-traits"; + version = "0.8.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "a53a94e5b2fd60417e83ffdfe136c39afacff0d4ac1d8d01cd66928ac610e1a2"; }; + }); + + "registry+https://github.com/rust-lang/crates.io-index".heed-types."0.8.0" = overridableMkRustCrate (profileName: rec { + name = "heed-types"; + version = "0.8.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "9a6cf0a6952fcedc992602d5cddd1e3fff091fbe87d38636e3ec23a31f32acbd"; }; + features = builtins.concatLists [ + [ "bincode" ] + [ "default" ] + [ "serde" ] + [ "serde-bincode" ] + [ "serde-json" ] + [ "serde_json" ] + ]; + dependencies = { + bincode = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bincode."1.3.3" { inherit profileName; }; + bytemuck = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytemuck."1.9.1" { inherit profileName; }; + byteorder = rustPackages."registry+https://github.com/rust-lang/crates.io-index".byteorder."1.4.3" { inherit profileName; }; + heed_traits = rustPackages."registry+https://github.com/rust-lang/crates.io-index".heed-traits."0.8.0" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".hermit-abi."0.1.19" = overridableMkRustCrate (profileName: rec { name = "hermit-abi"; version = "0.1.19"; @@ -1882,7 +2069,7 @@ in src = fetchCratesIo { inherit name version; sha256 = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2"; }; features = builtins.concatLists [ (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "client") - [ "default" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "full") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "h2") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "http1") @@ -1894,22 +2081,22 @@ in (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "tcp") ]; dependencies = { - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; - futures_channel = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-channel."0.3.21" { inherit profileName; }; - futures_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; - futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_channel" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-channel."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "h2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".h2."0.3.12" { inherit profileName; }; - http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; - http_body = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."0.4.4" { inherit profileName; }; - httparse = rustPackages."registry+https://github.com/rust-lang/crates.io-index".httparse."1.6.0" { inherit profileName; }; - httpdate = rustPackages."registry+https://github.com/rust-lang/crates.io-index".httpdate."1.0.2" { inherit profileName; }; - itoa = rustPackages."registry+https://github.com/rust-lang/crates.io-index".itoa."1.0.1" { inherit profileName; }; - pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "http" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "http_body" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."0.4.4" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "httparse" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".httparse."1.6.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "httpdate" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".httpdate."1.0.2" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "itoa" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".itoa."1.0.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "pin_project_lite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "socket2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".socket2."0.4.4" { inherit profileName; }; - tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; - tower_service = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.1" { inherit profileName; }; - tracing = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; - want = rustPackages."registry+https://github.com/rust-lang/crates.io-index".want."0.3.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tower_service" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tracing" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "want" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".want."0.3.0" { inherit profileName; }; }; }); @@ -2376,6 +2563,27 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".libsqlite3-sys."0.24.2" = overridableMkRustCrate (profileName: rec { + name = "libsqlite3-sys"; + version = "0.24.2"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14"; }; + features = builtins.concatLists [ + [ "bundled" ] + [ "bundled_bindings" ] + [ "cc" ] + [ "default" ] + [ "min_sqlite_version_3_6_8" ] + [ "pkg-config" ] + [ "vcpkg" ] + ]; + buildDependencies = { + cc = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".cc."1.0.73" { profileName = "__noProfile"; }; + pkg_config = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".pkg-config."0.3.24" { profileName = "__noProfile"; }; + vcpkg = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".vcpkg."0.2.15" { profileName = "__noProfile"; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".linked-hash-map."0.5.4" = overridableMkRustCrate (profileName: rec { name = "linked-hash-map"; version = "0.5.4"; @@ -2383,6 +2591,23 @@ in src = fetchCratesIo { inherit name version; sha256 = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"; }; }); + "registry+https://github.com/rust-lang/crates.io-index".lmdb-rkv-sys."0.11.2" = overridableMkRustCrate (profileName: rec { + name = "lmdb-rkv-sys"; + version = "0.11.2"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "61b9ce6b3be08acefa3003c57b7565377432a89ec24476bbe72e11d101f852fe"; }; + features = builtins.concatLists [ + [ "default" ] + ]; + dependencies = { + libc = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + }; + buildDependencies = { + cc = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".cc."1.0.73" { profileName = "__noProfile"; }; + pkg_config = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".pkg-config."0.3.24" { profileName = "__noProfile"; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".lock_api."0.4.6" = overridableMkRustCrate (profileName: rec { name = "lock_api"; version = "0.4.6"; @@ -2399,7 +2624,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8"; }; features = builtins.concatLists [ - (lib.optional (rootFeatures' ? "garage") "std") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "std") ]; dependencies = { cfg_if = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }; @@ -2493,7 +2718,7 @@ in [ "os-poll" ] ]; dependencies = { - ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "wasi" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "wasi" || hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if hostPlatform.isWindows then "miow" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".miow."0.3.7" { inherit profileName; }; ${ if hostPlatform.isWindows then "ntapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ntapi."0.3.7" { inherit profileName; }; @@ -2512,6 +2737,16 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".mktemp."0.4.1" = overridableMkRustCrate (profileName: rec { + name = "mktemp"; + version = "0.4.1"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "975de676448231fcde04b9149d2543077e166b78fc29eae5aa219e7928410da2"; }; + dependencies = { + uuid = rustPackages."registry+https://github.com/rust-lang/crates.io-index".uuid."0.8.2" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".multer."2.0.2" = overridableMkRustCrate (profileName: rec { name = "multer"; version = "2.0.2"; @@ -2594,31 +2829,31 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "c6419a4b836774192e13fedb05c0e5f414ee8df9ca0c467456a0bacde06c29ee"; }; features = builtins.concatLists [ - [ "default" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "opentelemetry") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "opentelemetry-contrib") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "rand") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "telemetry") ]; dependencies = { - arc_swap = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; - async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."0.6.0" { inherit profileName; }; - cfg_if = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }; - err_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.2.4" { profileName = "__noProfile"; }; - futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; - hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; - kuska_handshake = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-handshake."0.2.0" { inherit profileName; }; - sodiumoxide = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; - log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "arc_swap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "async_trait" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."0.6.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "cfg_if" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "err_derive" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.2.4" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "kuska_handshake" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-handshake."0.2.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "sodiumoxide" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "log" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "opentelemetry_contrib" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-contrib."0.9.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.5.6" { inherit profileName; }; - rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.14.4" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; - tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; - tokio_stream = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.8" { inherit profileName; }; - tokio_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.6.9" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.14.4" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio_stream" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.8" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.6.9" { inherit profileName; }; }; }); @@ -2662,10 +2897,10 @@ in (lib.optional (rootFeatures' ? "garage") "std") ]; dependencies = { - num_traits = rustPackages."registry+https://github.com/rust-lang/crates.io-index".num-traits."0.2.14" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "num_traits" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".num-traits."0.2.14" { inherit profileName; }; }; buildDependencies = { - autocfg = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".autocfg."1.1.0" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "autocfg" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".autocfg."1.1.0" { profileName = "__noProfile"; }; }; }); @@ -2900,6 +3135,17 @@ in ]; }); + "registry+https://github.com/rust-lang/crates.io-index".page_size."0.4.2" = overridableMkRustCrate (profileName: rec { + name = "page_size"; + version = "0.4.2"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "eebde548fbbf1ea81a99b128872779c437752fb99f217c45245e1a61dcd9edcd"; }; + dependencies = { + ${ if hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.isWindows then "winapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".winapi."0.3.9" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" = overridableMkRustCrate (profileName: rec { name = "parking_lot"; version = "0.11.2"; @@ -3320,19 +3566,19 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"; }; features = builtins.concatLists [ - [ "alloc" ] - [ "default" ] - [ "getrandom" ] - [ "libc" ] - [ "rand_chacha" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "alloc") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "getrandom") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "libc") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "rand_chacha") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "small_rng") - [ "std" ] - [ "std_rng" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "std") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "std_rng") ]; dependencies = { - ${ if hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - rand_chacha = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_chacha."0.3.1" { inherit profileName; }; - rand_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_core."0.6.3" { inherit profileName; }; + ${ if (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") && hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rand_chacha" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_chacha."0.3.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rand_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_core."0.6.3" { inherit profileName; }; }; }); @@ -3418,28 +3664,28 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"; }; features = builtins.concatLists [ - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "aho-corasick") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "default") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "memchr") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "perf") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "perf-cache") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "perf-dfa") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "perf-inline") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "perf-literal") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "std") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode-age") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode-bool") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode-case") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode-gencat") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode-perl") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode-script") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode-segment") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "aho-corasick") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "default") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "memchr") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "perf") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "perf-cache") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "perf-dfa") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "perf-inline") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "perf-literal") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "std") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "unicode") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "unicode-age") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "unicode-bool") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "unicode-case") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "unicode-gencat") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "unicode-perl") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "unicode-script") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "unicode-segment") ]; dependencies = { - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "aho_corasick" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aho-corasick."0.7.18" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "memchr" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "regex_syntax" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex-syntax."0.6.25" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web" then "aho_corasick" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aho-corasick."0.7.18" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web" then "memchr" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "regex_syntax" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex-syntax."0.6.25" { inherit profileName; }; }; }); @@ -3625,6 +3871,26 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".rusqlite."0.27.0" = overridableMkRustCrate (profileName: rec { + name = "rusqlite"; + version = "0.27.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a"; }; + features = builtins.concatLists [ + [ "bundled" ] + [ "modern_sqlite" ] + ]; + dependencies = { + bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; + fallible_iterator = rustPackages."registry+https://github.com/rust-lang/crates.io-index".fallible-iterator."0.2.0" { inherit profileName; }; + fallible_streaming_iterator = rustPackages."registry+https://github.com/rust-lang/crates.io-index".fallible-streaming-iterator."0.1.9" { inherit profileName; }; + hashlink = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hashlink."0.7.0" { inherit profileName; }; + libsqlite3_sys = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libsqlite3-sys."0.24.2" { inherit profileName; }; + memchr = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; }; + smallvec = rustPackages."registry+https://github.com/rust-lang/crates.io-index".smallvec."1.8.0" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".rustc_version."0.4.0" = overridableMkRustCrate (profileName: rec { name = "rustc_version"; version = "0.4.0"; @@ -3807,7 +4073,7 @@ in features = builtins.concatLists [ [ "default" ] [ "derive" ] - [ "rc" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "rc") [ "serde_derive" ] [ "std" ] ]; @@ -4136,7 +4402,7 @@ in [ "proc-macro" ] [ "quote" ] [ "visit" ] - [ "visit-mut" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "visit-mut") ]; dependencies = { proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; @@ -4145,6 +4411,16 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".synchronoise."1.0.0" = overridableMkRustCrate (profileName: rec { + name = "synchronoise"; + version = "1.0.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "d717ed0efc9d39ab3b642a096bc369a3e02a38a51c41845d7fe31bdad1d6eaeb"; }; + dependencies = { + crossbeam_queue = rustPackages."registry+https://github.com/rust-lang/crates.io-index".crossbeam-queue."0.1.2" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".synstructure."0.12.6" = overridableMkRustCrate (profileName: rec { name = "synstructure"; version = "0.12.6"; @@ -4283,44 +4559,44 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee"; }; features = builtins.concatLists [ - [ "bytes" ] - [ "default" ] - [ "fs" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "bytes") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "fs") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "full") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "io-std") - [ "io-util" ] - [ "libc" ] - [ "macros" ] - [ "memchr" ] - [ "mio" ] - [ "net" ] - [ "num_cpus" ] - [ "once_cell" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "io-util") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "libc") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "macros") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "memchr") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "mio") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "net") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "num_cpus") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "once_cell") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "parking_lot") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "process") - [ "rt" ] - [ "rt-multi-thread" ] - [ "signal" ] - [ "signal-hook-registry" ] - [ "socket2" ] - [ "sync" ] - [ "time" ] - [ "tokio-macros" ] - [ "winapi" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "rt") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "rt-multi-thread") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "signal") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "signal-hook-registry") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "socket2") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "sync") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "time") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "tokio-macros") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "winapi") ]; dependencies = { - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; - ${ if hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - memchr = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; }; - mio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".mio."0.8.2" { inherit profileName; }; - num_cpus = rustPackages."registry+https://github.com/rust-lang/crates.io-index".num_cpus."1.13.1" { inherit profileName; }; - once_cell = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + ${ if (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") && hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "memchr" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "mio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".mio."0.8.2" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "num_cpus" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".num_cpus."1.13.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.12.0" { inherit profileName; }; - pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; - ${ if hostPlatform.isUnix then "signal_hook_registry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".signal-hook-registry."1.4.0" { inherit profileName; }; - socket2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".socket2."0.4.4" { inherit profileName; }; - tokio_macros = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-macros."1.7.0" { profileName = "__noProfile"; }; - ${ if hostPlatform.isWindows then "winapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".winapi."0.3.9" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "pin_project_lite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; + ${ if (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") && hostPlatform.isUnix then "signal_hook_registry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".signal-hook-registry."1.4.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "socket2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".socket2."0.4.4" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio_macros" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-macros."1.7.0" { profileName = "__noProfile"; }; + ${ if (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") && hostPlatform.isWindows then "winapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".winapi."0.3.9" { inherit profileName; }; }; }); @@ -4376,14 +4652,14 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3"; }; features = builtins.concatLists [ - [ "default" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "net") - [ "time" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "time") ]; dependencies = { - futures_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; - pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; - tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "pin_project_lite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; }; }); @@ -4394,22 +4670,22 @@ in src = fetchCratesIo { inherit name version; sha256 = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0"; }; features = builtins.concatLists [ (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "codec") - [ "compat" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "compat") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") - [ "futures-io" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "futures-io") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "io") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "slab") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "time") ]; dependencies = { - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; - futures_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; - futures_io = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-io."0.3.21" { inherit profileName; }; - futures_sink = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-sink."0.3.21" { inherit profileName; }; - log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; - pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_io" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-io."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_sink" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-sink."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "log" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "pin_project_lite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "slab" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".slab."0.4.5" { inherit profileName; }; - tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; }; }); @@ -4599,19 +4875,19 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "4a1bdf54a7c28a2bbf701e1d2233f6c77f473486b94bee4f9678da5a148dca7f"; }; features = builtins.concatLists [ - [ "attributes" ] - [ "default" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "attributes") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "log") (lib.optional (rootFeatures' ? "garage") "log-always") - [ "std" ] - [ "tracing-attributes" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "std") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "tracing-attributes") ]; dependencies = { - cfg_if = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "cfg_if" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "log" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; - pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; - tracing_attributes = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-attributes."0.1.20" { profileName = "__noProfile"; }; - tracing_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-core."0.1.23" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "pin_project_lite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tracing_attributes" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-attributes."0.1.20" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tracing_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-core."0.1.23" { inherit profileName; }; }; }); @@ -4758,6 +5034,22 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".uuid."0.8.2" = overridableMkRustCrate (profileName: rec { + name = "uuid"; + version = "0.8.2"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"; }; + features = builtins.concatLists [ + [ "default" ] + [ "getrandom" ] + [ "std" ] + [ "v4" ] + ]; + dependencies = { + getrandom = rustPackages."registry+https://github.com/rust-lang/crates.io-index".getrandom."0.2.5" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".vcpkg."0.2.15" = overridableMkRustCrate (profileName: rec { name = "vcpkg"; version = "0.2.15"; @@ -4940,49 +5232,49 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"; }; features = builtins.concatLists [ - [ "cfg" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "cfg") [ "consoleapi" ] [ "errhandlingapi" ] - [ "evntrace" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "evntrace") [ "fileapi" ] [ "handleapi" ] - [ "in6addr" ] - [ "inaddr" ] - [ "ioapiset" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "in6addr") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "inaddr") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "ioapiset") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "knownfolders") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "lmcons") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "minschannel") [ "minwinbase" ] [ "minwindef" ] - [ "mswsock" ] - [ "namedpipeapi" ] - [ "ntdef" ] - [ "ntsecapi" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "mswsock") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "namedpipeapi") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "ntdef") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "ntsecapi") [ "ntstatus" ] (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "objbase") [ "processenv" ] [ "processthreadsapi" ] - [ "profileapi" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "profileapi") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "schannel") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "securitybaseapi") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "shlobj") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "sspi") [ "std" ] - [ "synchapi" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "synchapi") [ "sysinfoapi" ] (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "threadpoollegacyapiset") - [ "timezoneapi" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "timezoneapi") [ "winbase" ] [ "wincon" ] (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "wincrypt") - [ "windef" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "windef") [ "winerror" ] - [ "winioctl" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "winioctl") [ "winnt" ] - [ "winsock2" ] - [ "ws2def" ] - [ "ws2ipdef" ] - [ "ws2tcpip" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "winsock2") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "ws2def") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "ws2ipdef") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "ws2tcpip") (lib.optional (rootFeatures' ? "garage") "wtypesbase") ]; dependencies = { @@ -5030,11 +5322,11 @@ in [ "default" ] ]; dependencies = { - ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); diff --git a/Cargo.toml b/Cargo.toml index edd0e3f9..122285db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "src/db", "src/util", "src/rpc", "src/table", diff --git a/src/api/admin/cluster.rs b/src/api/admin/cluster.rs index 6d01317d..4b7716a3 100644 --- a/src/api/admin/cluster.rs +++ b/src/api/admin/cluster.rs @@ -19,6 +19,7 @@ pub async fn handle_get_cluster_status(garage: &Arc) -> Result) -> GetClusterLayoutResponse { struct GetClusterStatusResponse { node: String, garage_version: &'static str, + db_engine: String, known_nodes: HashMap, layout: GetClusterLayoutResponse, } diff --git a/src/block/Cargo.toml b/src/block/Cargo.toml index 9cba69ee..80346aca 100644 --- a/src/block/Cargo.toml +++ b/src/block/Cargo.toml @@ -14,6 +14,7 @@ path = "lib.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +garage_db = { version = "0.8.0", path = "../db" } garage_rpc = { version = "0.7.0", path = "../rpc" } garage_util = { version = "0.7.0", path = "../util" } garage_table = { version = "0.7.0", path = "../table" } @@ -27,8 +28,6 @@ tracing = "0.1.30" rand = "0.8" zstd = { version = "0.9", default-features = false } -sled = "0.34" - rmp-serde = "0.15" serde = { version = "1.0", default-features = false, features = ["derive", "rc"] } serde_bytes = "0.11" diff --git a/src/block/manager.rs b/src/block/manager.rs index 9b2d9cad..32ba0431 100644 --- a/src/block/manager.rs +++ b/src/block/manager.rs @@ -1,3 +1,5 @@ +use core::ops::Bound; + use std::convert::TryInto; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -17,10 +19,12 @@ use opentelemetry::{ Context, KeyValue, }; +use garage_db as db; +use garage_db::counted_tree_hack::CountedTree; + use garage_util::data::*; use garage_util::error::*; use garage_util::metrics::RecordDuration; -use garage_util::sled_counter::SledCountedTree; use garage_util::time::*; use garage_util::tranquilizer::Tranquilizer; @@ -91,9 +95,9 @@ pub struct BlockManager { rc: BlockRc, - resync_queue: SledCountedTree, + resync_queue: CountedTree, resync_notify: Notify, - resync_errors: SledCountedTree, + resync_errors: CountedTree, system: Arc, endpoint: Arc>, @@ -108,7 +112,7 @@ struct BlockManagerLocked(); impl BlockManager { pub fn new( - db: &sled::Db, + db: &db::Db, data_dir: PathBuf, compression_level: Option, background_tranquility: u32, @@ -123,12 +127,14 @@ impl BlockManager { let resync_queue = db .open_tree("block_local_resync_queue") .expect("Unable to open block_local_resync_queue tree"); - let resync_queue = SledCountedTree::new(resync_queue); + let resync_queue = + CountedTree::new(resync_queue).expect("Could not count block_local_resync_queue"); let resync_errors = db .open_tree("block_local_resync_errors") .expect("Unable to open block_local_resync_errors tree"); - let resync_errors = SledCountedTree::new(resync_errors); + let resync_errors = + CountedTree::new(resync_errors).expect("Could not count block_local_resync_errors"); let endpoint = system .netapp @@ -219,11 +225,44 @@ impl BlockManager { /// to fix any mismatch between the two. pub async fn repair_data_store(&self, must_exit: &watch::Receiver) -> Result<(), Error> { // 1. Repair blocks from RC table. - for (i, entry) in self.rc.rc.iter().enumerate() { - let (hash, _) = entry?; - let hash = Hash::try_from(&hash[..]).unwrap(); - self.put_to_resync(&hash, Duration::from_secs(0))?; - if i & 0xFF == 0 && *must_exit.borrow() { + let mut next_start: Option = None; + loop { + // We have to do this complicated two-step process where we first read a bunch + // of hashes from the RC table, and then insert them in the to-resync queue, + // because of SQLite. Basically, as long as we have an iterator on a DB table, + // we can't do anything else on the DB. The naive approach (which we had previously) + // of just iterating on the RC table and inserting items one to one in the resync + // queue can't work here, it would just provoke a deadlock in the SQLite adapter code. + // This is mostly because the Rust bindings for SQLite assume a worst-case scenario + // where SQLite is not compiled in thread-safe mode, so we have to wrap everything + // in a mutex (see db/sqlite_adapter.rs and discussion in PR #322). + let mut batch_of_hashes = vec![]; + let start_bound = match next_start.as_ref() { + None => Bound::Unbounded, + Some(x) => Bound::Excluded(x.as_slice()), + }; + for entry in self + .rc + .rc + .range::<&[u8], _>((start_bound, Bound::Unbounded))? + { + let (hash, _) = entry?; + let hash = Hash::try_from(&hash[..]).unwrap(); + batch_of_hashes.push(hash); + if batch_of_hashes.len() >= 1000 { + break; + } + } + if batch_of_hashes.is_empty() { + break; + } + + for hash in batch_of_hashes.into_iter() { + self.put_to_resync(&hash, Duration::from_secs(0))?; + next_start = Some(hash) + } + + if *must_exit.borrow() { return Ok(()); } } @@ -264,46 +303,69 @@ impl BlockManager { } /// Get lenght of resync queue - pub fn resync_queue_len(&self) -> usize { - self.resync_queue.len() + pub fn resync_queue_len(&self) -> Result { + // This currently can't return an error because the CountedTree hack + // doesn't error on .len(), but this will change when we remove the hack + // (hopefully someday!) + Ok(self.resync_queue.len()) } /// Get number of blocks that have an error - pub fn resync_errors_len(&self) -> usize { - self.resync_errors.len() + pub fn resync_errors_len(&self) -> Result { + // (see resync_queue_len comment) + Ok(self.resync_errors.len()) } /// Get number of items in the refcount table - pub fn rc_len(&self) -> usize { - self.rc.rc.len() + pub fn rc_len(&self) -> Result { + Ok(self.rc.rc.len()?) } //// ----- Managing the reference counter ---- /// Increment the number of time a block is used, putting it to resynchronization if it is /// required, but not known - pub fn block_incref(&self, hash: &Hash) -> Result<(), Error> { - if self.rc.block_incref(hash)? { + pub fn block_incref( + self: &Arc, + tx: &mut db::Transaction, + hash: Hash, + ) -> db::TxOpResult<()> { + if self.rc.block_incref(tx, &hash)? { // When the reference counter is incremented, there is // normally a node that is responsible for sending us the // data of the block. However that operation may fail, // so in all cases we add the block here to the todo list // to check later that it arrived correctly, and if not // we will fecth it from someone. - self.put_to_resync(hash, 2 * BLOCK_RW_TIMEOUT)?; + let this = self.clone(); + tokio::spawn(async move { + if let Err(e) = this.put_to_resync(&hash, 2 * BLOCK_RW_TIMEOUT) { + error!("Block {:?} could not be put in resync queue: {}.", hash, e); + } + }); } Ok(()) } /// Decrement the number of time a block is used - pub fn block_decref(&self, hash: &Hash) -> Result<(), Error> { - if self.rc.block_decref(hash)? { + pub fn block_decref( + self: &Arc, + tx: &mut db::Transaction, + hash: Hash, + ) -> db::TxOpResult<()> { + if self.rc.block_decref(tx, &hash)? { // When the RC is decremented, it might drop to zero, // indicating that we don't need the block. // There is a delay before we garbage collect it; // make sure that it is handled in the resync loop // after that delay has passed. - self.put_to_resync(hash, BLOCK_GC_DELAY + Duration::from_secs(10))?; + let this = self.clone(); + tokio::spawn(async move { + if let Err(e) = this.put_to_resync(&hash, BLOCK_GC_DELAY + Duration::from_secs(10)) + { + error!("Block {:?} could not be put in resync queue: {}.", hash, e); + } + }); } Ok(()) } @@ -503,12 +565,12 @@ impl BlockManager { }); } - fn put_to_resync(&self, hash: &Hash, delay: Duration) -> Result<(), sled::Error> { + fn put_to_resync(&self, hash: &Hash, delay: Duration) -> db::Result<()> { let when = now_msec() + delay.as_millis() as u64; self.put_to_resync_at(hash, when) } - fn put_to_resync_at(&self, hash: &Hash, when: u64) -> Result<(), sled::Error> { + fn put_to_resync_at(&self, hash: &Hash, when: u64) -> db::Result<()> { trace!("Put resync_queue: {} {:?}", when, hash); let mut key = u64::to_be_bytes(when).to_vec(); key.extend(hash.as_ref()); @@ -547,13 +609,8 @@ impl BlockManager { // - Ok(true) -> a block was processed (successfully or not) // - Ok(false) -> no block was processed, but we are ready for the next iteration // - Err(_) -> a Sled error occurred when reading/writing from resync_queue/resync_errors - async fn resync_iter( - &self, - must_exit: &mut watch::Receiver, - ) -> Result { - if let Some(first_pair_res) = self.resync_queue.iter().next() { - let (time_bytes, hash_bytes) = first_pair_res?; - + async fn resync_iter(&self, must_exit: &mut watch::Receiver) -> Result { + if let Some((time_bytes, hash_bytes)) = self.resync_queue.first()? { let time_msec = u64::from_be_bytes(time_bytes[0..8].try_into().unwrap()); let now = now_msec(); @@ -561,7 +618,7 @@ impl BlockManager { let hash = Hash::try_from(&hash_bytes[..]).unwrap(); if let Some(ec) = self.resync_errors.get(hash.as_slice())? { - let ec = ErrorCounter::decode(ec); + let ec = ErrorCounter::decode(&ec); if now < ec.next_try() { // if next retry after an error is not yet, // don't do resync and return early, but still @@ -602,7 +659,7 @@ impl BlockManager { warn!("Error when resyncing {:?}: {}", hash, e); let err_counter = match self.resync_errors.get(hash.as_slice())? { - Some(ec) => ErrorCounter::decode(ec).add1(now + 1), + Some(ec) => ErrorCounter::decode(&ec).add1(now + 1), None => ErrorCounter::new(now + 1), }; @@ -966,7 +1023,7 @@ impl ErrorCounter { } } - fn decode(data: sled::IVec) -> Self { + fn decode(data: &[u8]) -> Self { Self { errors: u64::from_be_bytes(data[0..8].try_into().unwrap()), last_try: u64::from_be_bytes(data[8..16].try_into().unwrap()), diff --git a/src/block/metrics.rs b/src/block/metrics.rs index f0f541a3..477add66 100644 --- a/src/block/metrics.rs +++ b/src/block/metrics.rs @@ -1,6 +1,6 @@ use opentelemetry::{global, metrics::*}; -use garage_util::sled_counter::SledCountedTree; +use garage_db::counted_tree_hack::CountedTree; /// TableMetrics reference all counter used for metrics pub struct BlockManagerMetrics { @@ -23,7 +23,7 @@ pub struct BlockManagerMetrics { } impl BlockManagerMetrics { - pub fn new(resync_queue: SledCountedTree, resync_errors: SledCountedTree) -> Self { + pub fn new(resync_queue: CountedTree, resync_errors: CountedTree) -> Self { let meter = global::meter("garage_model/block"); Self { _resync_queue_len: meter diff --git a/src/block/rc.rs b/src/block/rc.rs index ec3ea44e..ce6defad 100644 --- a/src/block/rc.rs +++ b/src/block/rc.rs @@ -1,5 +1,7 @@ use std::convert::TryInto; +use garage_db as db; + use garage_util::data::*; use garage_util::error::*; use garage_util::time::*; @@ -7,31 +9,41 @@ use garage_util::time::*; use crate::manager::BLOCK_GC_DELAY; pub struct BlockRc { - pub(crate) rc: sled::Tree, + pub(crate) rc: db::Tree, } impl BlockRc { - pub(crate) fn new(rc: sled::Tree) -> Self { + pub(crate) fn new(rc: db::Tree) -> Self { Self { rc } } /// Increment the reference counter associated to a hash. /// Returns true if the RC goes from zero to nonzero. - pub(crate) fn block_incref(&self, hash: &Hash) -> Result { - let old_rc = self - .rc - .fetch_and_update(&hash, |old| RcEntry::parse_opt(old).increment().serialize())?; - let old_rc = RcEntry::parse_opt(old_rc); + pub(crate) fn block_incref( + &self, + tx: &mut db::Transaction, + hash: &Hash, + ) -> db::TxOpResult { + let old_rc = RcEntry::parse_opt(tx.get(&self.rc, &hash)?); + match old_rc.increment().serialize() { + Some(x) => tx.insert(&self.rc, &hash, x)?, + None => unreachable!(), + }; Ok(old_rc.is_zero()) } /// Decrement the reference counter associated to a hash. /// Returns true if the RC is now zero. - pub(crate) fn block_decref(&self, hash: &Hash) -> Result { - let new_rc = self - .rc - .update_and_fetch(&hash, |old| RcEntry::parse_opt(old).decrement().serialize())?; - let new_rc = RcEntry::parse_opt(new_rc); + pub(crate) fn block_decref( + &self, + tx: &mut db::Transaction, + hash: &Hash, + ) -> db::TxOpResult { + let new_rc = RcEntry::parse_opt(tx.get(&self.rc, &hash)?).decrement(); + match new_rc.serialize() { + Some(x) => tx.insert(&self.rc, &hash, x)?, + None => tx.remove(&self.rc, &hash)?, + }; Ok(matches!(new_rc, RcEntry::Deletable { .. })) } @@ -44,12 +56,15 @@ impl BlockRc { /// deletion time has passed pub(crate) fn clear_deleted_block_rc(&self, hash: &Hash) -> Result<(), Error> { let now = now_msec(); - self.rc.update_and_fetch(&hash, |rcval| { - let updated = match RcEntry::parse_opt(rcval) { - RcEntry::Deletable { at_time } if now > at_time => RcEntry::Absent, - v => v, + self.rc.db().transaction(|mut tx| { + let rcval = RcEntry::parse_opt(tx.get(&self.rc, &hash)?); + match rcval { + RcEntry::Deletable { at_time } if now > at_time => { + tx.remove(&self.rc, &hash)?; + } + _ => (), }; - updated.serialize() + tx.commit(()) })?; Ok(()) } diff --git a/src/db/Cargo.toml b/src/db/Cargo.toml new file mode 100644 index 00000000..6d8f64be --- /dev/null +++ b/src/db/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "garage_db" +version = "0.8.0" +authors = ["Alex Auvolat "] +edition = "2018" +license = "AGPL-3.0" +description = "Abstraction over multiple key/value storage engines that supports transactions" +repository = "https://git.deuxfleurs.fr/Deuxfleurs/garage" +readme = "../../README.md" + +[lib] +path = "lib.rs" + +[[bin]] +name = "convert" +path = "bin/convert.rs" +required-features = ["cli"] + +[dependencies] +err-derive = "0.3" +hexdump = "0.1" +log = "0.4" + +heed = "0.11" +rusqlite = { version = "0.27", features = ["bundled"] } +sled = "0.34" + +# cli deps +clap = { version = "3.1.18", optional = true, features = ["derive", "env"] } +pretty_env_logger = { version = "0.4", optional = true } + +[dev-dependencies] +mktemp = "0.4" + +[features] +cli = ["clap", "pretty_env_logger"] diff --git a/src/db/bin/convert.rs b/src/db/bin/convert.rs new file mode 100644 index 00000000..9e45e61f --- /dev/null +++ b/src/db/bin/convert.rs @@ -0,0 +1,76 @@ +use std::path::PathBuf; + +use garage_db::*; + +use clap::Parser; + +/// K2V command line interface +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Args { + /// Input DB path + #[clap(short = 'i')] + input_path: PathBuf, + /// Input DB engine + #[clap(short = 'a')] + input_engine: String, + + /// Output DB path + #[clap(short = 'o')] + output_path: PathBuf, + /// Output DB engine + #[clap(short = 'b')] + output_engine: String, +} + +fn main() { + let args = Args::parse(); + pretty_env_logger::init(); + + match do_conversion(args) { + Ok(()) => println!("Success!"), + Err(e) => eprintln!("Error: {}", e), + } +} + +fn do_conversion(args: Args) -> Result<()> { + let input = open_db(args.input_path, args.input_engine)?; + let output = open_db(args.output_path, args.output_engine)?; + output.import(&input)?; + Ok(()) +} + +fn open_db(path: PathBuf, engine: String) -> Result { + match engine.as_str() { + "sled" => { + let db = sled_adapter::sled::Config::default().path(&path).open()?; + Ok(sled_adapter::SledDb::init(db)) + } + "sqlite" | "sqlite3" | "rusqlite" => { + let db = sqlite_adapter::rusqlite::Connection::open(&path)?; + Ok(sqlite_adapter::SqliteDb::init(db)) + } + "lmdb" | "heed" => { + std::fs::create_dir_all(&path).map_err(|e| { + Error(format!("Unable to create LMDB data directory: {}", e).into()) + })?; + + let map_size = if u32::MAX as usize == usize::MAX { + eprintln!( + "LMDB is not recommended on 32-bit systems, database size will be limited" + ); + 1usize << 30 // 1GB for 32-bit systems + } else { + 1usize << 40 // 1TB for 64-bit systems + }; + + let db = lmdb_adapter::heed::EnvOpenOptions::new() + .max_dbs(100) + .map_size(map_size) + .open(&path) + .unwrap(); + Ok(lmdb_adapter::LmdbDb::init(db)) + } + e => Err(Error(format!("Invalid DB engine: {}", e).into())), + } +} diff --git a/src/db/counted_tree_hack.rs b/src/db/counted_tree_hack.rs new file mode 100644 index 00000000..bbe943a2 --- /dev/null +++ b/src/db/counted_tree_hack.rs @@ -0,0 +1,127 @@ +//! This hack allows a db tree to keep in RAM a counter of the number of entries +//! it contains, which is used to call .len() on it. This is usefull only for +//! the sled backend where .len() otherwise would have to traverse the whole +//! tree to count items. For sqlite and lmdb, this is mostly useless (but +//! hopefully not harmfull!). Note that a CountedTree cannot be part of a +//! transaction. + +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, +}; + +use crate::{Result, Tree, TxError, Value, ValueIter}; + +#[derive(Clone)] +pub struct CountedTree(Arc); + +struct CountedTreeInternal { + tree: Tree, + len: AtomicUsize, +} + +impl CountedTree { + pub fn new(tree: Tree) -> Result { + let len = tree.len()?; + Ok(Self(Arc::new(CountedTreeInternal { + tree, + len: AtomicUsize::new(len), + }))) + } + + pub fn len(&self) -> usize { + self.0.len.load(Ordering::SeqCst) + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn get>(&self, key: K) -> Result> { + self.0.tree.get(key) + } + + pub fn first(&self) -> Result> { + self.0.tree.first() + } + + pub fn iter(&self) -> Result> { + self.0.tree.iter() + } + + // ---- writing functions ---- + + pub fn insert(&self, key: K, value: V) -> Result> + where + K: AsRef<[u8]>, + V: AsRef<[u8]>, + { + let old_val = self.0.tree.insert(key, value)?; + if old_val.is_none() { + self.0.len.fetch_add(1, Ordering::SeqCst); + } + Ok(old_val) + } + + pub fn remove>(&self, key: K) -> Result> { + let old_val = self.0.tree.remove(key)?; + if old_val.is_some() { + self.0.len.fetch_sub(1, Ordering::SeqCst); + } + Ok(old_val) + } + + pub fn compare_and_swap( + &self, + key: K, + expected_old: Option, + new: Option, + ) -> Result + where + K: AsRef<[u8]>, + OV: AsRef<[u8]>, + NV: AsRef<[u8]>, + { + let old_some = expected_old.is_some(); + let new_some = new.is_some(); + + let tx_res = self.0.tree.db().transaction(|mut tx| { + let old_val = tx.get(&self.0.tree, &key)?; + let is_same = match (&old_val, &expected_old) { + (None, None) => true, + (Some(x), Some(y)) if x == y.as_ref() => true, + _ => false, + }; + if is_same { + match &new { + Some(v) => { + tx.insert(&self.0.tree, &key, v)?; + } + None => { + tx.remove(&self.0.tree, &key)?; + } + } + tx.commit(()) + } else { + tx.abort(()) + } + }); + + match tx_res { + Ok(()) => { + match (old_some, new_some) { + (false, true) => { + self.0.len.fetch_add(1, Ordering::SeqCst); + } + (true, false) => { + self.0.len.fetch_sub(1, Ordering::SeqCst); + } + _ => (), + } + Ok(true) + } + Err(TxError::Abort(())) => Ok(false), + Err(TxError::Db(e)) => Err(e), + } + } +} diff --git a/src/db/lib.rs b/src/db/lib.rs new file mode 100644 index 00000000..e9d3ea18 --- /dev/null +++ b/src/db/lib.rs @@ -0,0 +1,400 @@ +pub mod lmdb_adapter; +pub mod sled_adapter; +pub mod sqlite_adapter; + +pub mod counted_tree_hack; + +#[cfg(test)] +pub mod test; + +use core::ops::{Bound, RangeBounds}; + +use std::borrow::Cow; +use std::cell::Cell; +use std::sync::Arc; + +use err_derive::Error; + +#[derive(Clone)] +pub struct Db(pub(crate) Arc); + +pub struct Transaction<'a>(&'a mut dyn ITx); + +#[derive(Clone)] +pub struct Tree(Arc, usize); + +pub type Value = Vec; +pub type ValueIter<'a> = Box> + 'a>; +pub type TxValueIter<'a> = Box> + 'a>; + +// ---- + +#[derive(Debug, Error)] +#[error(display = "{}", _0)] +pub struct Error(pub Cow<'static, str>); + +pub type Result = std::result::Result; + +#[derive(Debug, Error)] +#[error(display = "{}", _0)] +pub struct TxOpError(pub(crate) Error); +pub type TxOpResult = std::result::Result; + +pub enum TxError { + Abort(E), + Db(Error), +} +pub type TxResult = std::result::Result>; + +impl From for TxError { + fn from(e: TxOpError) -> TxError { + TxError::Db(e.0) + } +} + +pub fn unabort(res: TxResult) -> TxOpResult> { + match res { + Ok(v) => Ok(Ok(v)), + Err(TxError::Abort(e)) => Ok(Err(e)), + Err(TxError::Db(e)) => Err(TxOpError(e)), + } +} + +// ---- + +impl Db { + pub fn engine(&self) -> String { + self.0.engine() + } + + pub fn open_tree>(&self, name: S) -> Result { + let tree_id = self.0.open_tree(name.as_ref())?; + Ok(Tree(self.0.clone(), tree_id)) + } + + pub fn list_trees(&self) -> Result> { + self.0.list_trees() + } + + pub fn transaction(&self, fun: F) -> TxResult + where + F: Fn(Transaction<'_>) -> TxResult, + { + let f = TxFn { + function: fun, + result: Cell::new(None), + }; + let tx_res = self.0.transaction(&f); + let ret = f + .result + .into_inner() + .expect("Transaction did not store result"); + + match tx_res { + Ok(()) => { + assert!(matches!(ret, Ok(_))); + ret + } + Err(TxError::Abort(())) => { + assert!(matches!(ret, Err(TxError::Abort(_)))); + ret + } + Err(TxError::Db(e2)) => match ret { + // Ok was stored -> the error occured when finalizing + // transaction + Ok(_) => Err(TxError::Db(e2)), + // An error was already stored: that's the one we want to + // return + Err(TxError::Db(e)) => Err(TxError::Db(e)), + _ => unreachable!(), + }, + } + } + + pub fn import(&self, other: &Db) -> Result<()> { + let existing_trees = self.list_trees()?; + if !existing_trees.is_empty() { + return Err(Error( + format!( + "destination database already contains data: {:?}", + existing_trees + ) + .into(), + )); + } + + let tree_names = other.list_trees()?; + for name in tree_names { + let tree = self.open_tree(&name)?; + if tree.len()? > 0 { + return Err(Error(format!("tree {} already contains data", name).into())); + } + + let ex_tree = other.open_tree(&name)?; + + let tx_res = self.transaction(|mut tx| { + let mut i = 0; + for item in ex_tree.iter().map_err(TxError::Abort)? { + let (k, v) = item.map_err(TxError::Abort)?; + tx.insert(&tree, k, v)?; + i += 1; + if i % 1000 == 0 { + println!("{}: imported {}", name, i); + } + } + tx.commit(i) + }); + let total = match tx_res { + Err(TxError::Db(e)) => return Err(e), + Err(TxError::Abort(e)) => return Err(e), + Ok(x) => x, + }; + + println!("{}: finished importing, {} items", name, total); + } + Ok(()) + } +} + +#[allow(clippy::len_without_is_empty)] +impl Tree { + #[inline] + pub fn db(&self) -> Db { + Db(self.0.clone()) + } + + #[inline] + pub fn get>(&self, key: T) -> Result> { + self.0.get(self.1, key.as_ref()) + } + #[inline] + pub fn len(&self) -> Result { + self.0.len(self.1) + } + + #[inline] + pub fn first(&self) -> Result> { + self.iter()?.next().transpose() + } + #[inline] + pub fn get_gt>(&self, from: T) -> Result> { + self.range((Bound::Excluded(from), Bound::Unbounded))? + .next() + .transpose() + } + + /// Returns the old value if there was one + #[inline] + pub fn insert, U: AsRef<[u8]>>( + &self, + key: T, + value: U, + ) -> Result> { + self.0.insert(self.1, key.as_ref(), value.as_ref()) + } + /// Returns the old value if there was one + #[inline] + pub fn remove>(&self, key: T) -> Result> { + self.0.remove(self.1, key.as_ref()) + } + + #[inline] + pub fn iter(&self) -> Result> { + self.0.iter(self.1) + } + #[inline] + pub fn iter_rev(&self) -> Result> { + self.0.iter_rev(self.1) + } + + #[inline] + pub fn range(&self, range: R) -> Result> + where + K: AsRef<[u8]>, + R: RangeBounds, + { + let sb = range.start_bound(); + let eb = range.end_bound(); + self.0.range(self.1, get_bound(sb), get_bound(eb)) + } + #[inline] + pub fn range_rev(&self, range: R) -> Result> + where + K: AsRef<[u8]>, + R: RangeBounds, + { + let sb = range.start_bound(); + let eb = range.end_bound(); + self.0.range_rev(self.1, get_bound(sb), get_bound(eb)) + } +} + +#[allow(clippy::len_without_is_empty)] +impl<'a> Transaction<'a> { + #[inline] + pub fn get>(&self, tree: &Tree, key: T) -> TxOpResult> { + self.0.get(tree.1, key.as_ref()) + } + #[inline] + pub fn len(&self, tree: &Tree) -> TxOpResult { + self.0.len(tree.1) + } + + /// Returns the old value if there was one + #[inline] + pub fn insert, U: AsRef<[u8]>>( + &mut self, + tree: &Tree, + key: T, + value: U, + ) -> TxOpResult> { + self.0.insert(tree.1, key.as_ref(), value.as_ref()) + } + /// Returns the old value if there was one + #[inline] + pub fn remove>(&mut self, tree: &Tree, key: T) -> TxOpResult> { + self.0.remove(tree.1, key.as_ref()) + } + + #[inline] + pub fn iter(&self, tree: &Tree) -> TxOpResult> { + self.0.iter(tree.1) + } + #[inline] + pub fn iter_rev(&self, tree: &Tree) -> TxOpResult> { + self.0.iter_rev(tree.1) + } + + #[inline] + pub fn range(&self, tree: &Tree, range: R) -> TxOpResult> + where + K: AsRef<[u8]>, + R: RangeBounds, + { + let sb = range.start_bound(); + let eb = range.end_bound(); + self.0.range(tree.1, get_bound(sb), get_bound(eb)) + } + #[inline] + pub fn range_rev(&self, tree: &Tree, range: R) -> TxOpResult> + where + K: AsRef<[u8]>, + R: RangeBounds, + { + let sb = range.start_bound(); + let eb = range.end_bound(); + self.0.range_rev(tree.1, get_bound(sb), get_bound(eb)) + } + + // ---- + + #[inline] + pub fn abort(self, e: E) -> TxResult { + Err(TxError::Abort(e)) + } + + #[inline] + pub fn commit(self, r: R) -> TxResult { + Ok(r) + } +} + +// ---- Internal interfaces + +pub(crate) trait IDb: Send + Sync { + fn engine(&self) -> String; + fn open_tree(&self, name: &str) -> Result; + fn list_trees(&self) -> Result>; + + fn get(&self, tree: usize, key: &[u8]) -> Result>; + fn len(&self, tree: usize) -> Result; + + fn insert(&self, tree: usize, key: &[u8], value: &[u8]) -> Result>; + fn remove(&self, tree: usize, key: &[u8]) -> Result>; + + fn iter(&self, tree: usize) -> Result>; + fn iter_rev(&self, tree: usize) -> Result>; + + fn range<'r>( + &self, + tree: usize, + low: Bound<&'r [u8]>, + high: Bound<&'r [u8]>, + ) -> Result>; + fn range_rev<'r>( + &self, + tree: usize, + low: Bound<&'r [u8]>, + high: Bound<&'r [u8]>, + ) -> Result>; + + fn transaction(&self, f: &dyn ITxFn) -> TxResult<(), ()>; +} + +pub(crate) trait ITx { + fn get(&self, tree: usize, key: &[u8]) -> TxOpResult>; + fn len(&self, tree: usize) -> TxOpResult; + + fn insert(&mut self, tree: usize, key: &[u8], value: &[u8]) -> TxOpResult>; + fn remove(&mut self, tree: usize, key: &[u8]) -> TxOpResult>; + + fn iter(&self, tree: usize) -> TxOpResult>; + fn iter_rev(&self, tree: usize) -> TxOpResult>; + + fn range<'r>( + &self, + tree: usize, + low: Bound<&'r [u8]>, + high: Bound<&'r [u8]>, + ) -> TxOpResult>; + fn range_rev<'r>( + &self, + tree: usize, + low: Bound<&'r [u8]>, + high: Bound<&'r [u8]>, + ) -> TxOpResult>; +} + +pub(crate) trait ITxFn { + fn try_on(&self, tx: &mut dyn ITx) -> TxFnResult; +} + +pub(crate) enum TxFnResult { + Ok, + Abort, + DbErr, +} + +struct TxFn +where + F: Fn(Transaction<'_>) -> TxResult, +{ + function: F, + result: Cell>>, +} + +impl ITxFn for TxFn +where + F: Fn(Transaction<'_>) -> TxResult, +{ + fn try_on(&self, tx: &mut dyn ITx) -> TxFnResult { + let res = (self.function)(Transaction(tx)); + let res2 = match &res { + Ok(_) => TxFnResult::Ok, + Err(TxError::Abort(_)) => TxFnResult::Abort, + Err(TxError::Db(_)) => TxFnResult::DbErr, + }; + self.result.set(Some(res)); + res2 + } +} + +// ---- + +fn get_bound>(b: Bound<&K>) -> Bound<&[u8]> { + match b { + Bound::Included(v) => Bound::Included(v.as_ref()), + Bound::Excluded(v) => Bound::Excluded(v.as_ref()), + Bound::Unbounded => Bound::Unbounded, + } +} diff --git a/src/db/lmdb_adapter.rs b/src/db/lmdb_adapter.rs new file mode 100644 index 00000000..74622919 --- /dev/null +++ b/src/db/lmdb_adapter.rs @@ -0,0 +1,329 @@ +use core::ops::Bound; +use core::ptr::NonNull; + +use std::collections::HashMap; +use std::convert::TryInto; +use std::sync::{Arc, RwLock}; + +use heed::types::ByteSlice; +use heed::{BytesDecode, Env, RoTxn, RwTxn, UntypedDatabase as Database}; + +use crate::{ + Db, Error, IDb, ITx, ITxFn, Result, TxError, TxFnResult, TxOpError, TxOpResult, TxResult, + TxValueIter, Value, ValueIter, +}; + +pub use heed; + +// -- err + +impl From for Error { + fn from(e: heed::Error) -> Error { + Error(format!("LMDB: {}", e).into()) + } +} + +impl From for TxOpError { + fn from(e: heed::Error) -> TxOpError { + TxOpError(e.into()) + } +} + +// -- db + +pub struct LmdbDb { + db: heed::Env, + trees: RwLock<(Vec, HashMap)>, +} + +impl LmdbDb { + pub fn init(db: Env) -> Db { + let s = Self { + db, + trees: RwLock::new((Vec::new(), HashMap::new())), + }; + Db(Arc::new(s)) + } + + fn get_tree(&self, i: usize) -> Result { + self.trees + .read() + .unwrap() + .0 + .get(i) + .cloned() + .ok_or_else(|| Error("invalid tree id".into())) + } +} + +impl IDb for LmdbDb { + fn engine(&self) -> String { + "LMDB (using Heed crate)".into() + } + + fn open_tree(&self, name: &str) -> Result { + let mut trees = self.trees.write().unwrap(); + if let Some(i) = trees.1.get(name) { + Ok(*i) + } else { + let tree = self.db.create_database(Some(name))?; + let i = trees.0.len(); + trees.0.push(tree); + trees.1.insert(name.to_string(), i); + Ok(i) + } + } + + fn list_trees(&self) -> Result> { + let tree0 = match self.db.open_database::(None)? { + Some(x) => x, + None => return Ok(vec![]), + }; + + let mut ret = vec![]; + let tx = self.db.read_txn()?; + for item in tree0.iter(&tx)? { + let (tree_name, _) = item?; + ret.push(tree_name.to_string()); + } + drop(tx); + + let mut ret2 = vec![]; + for tree_name in ret { + if self + .db + .open_database::(Some(&tree_name))? + .is_some() + { + ret2.push(tree_name); + } + } + + Ok(ret2) + } + + // ---- + + fn get(&self, tree: usize, key: &[u8]) -> Result> { + let tree = self.get_tree(tree)?; + + let tx = self.db.read_txn()?; + let val = tree.get(&tx, key)?; + match val { + None => Ok(None), + Some(v) => Ok(Some(v.to_vec())), + } + } + + fn len(&self, tree: usize) -> Result { + let tree = self.get_tree(tree)?; + let tx = self.db.read_txn()?; + Ok(tree.len(&tx)?.try_into().unwrap()) + } + + fn insert(&self, tree: usize, key: &[u8], value: &[u8]) -> Result> { + let tree = self.get_tree(tree)?; + let mut tx = self.db.write_txn()?; + let old_val = tree.get(&tx, key)?.map(Vec::from); + tree.put(&mut tx, key, value)?; + tx.commit()?; + Ok(old_val) + } + + fn remove(&self, tree: usize, key: &[u8]) -> Result> { + let tree = self.get_tree(tree)?; + let mut tx = self.db.write_txn()?; + let old_val = tree.get(&tx, key)?.map(Vec::from); + tree.delete(&mut tx, key)?; + tx.commit()?; + Ok(old_val) + } + + fn iter(&self, tree: usize) -> Result> { + let tree = self.get_tree(tree)?; + let tx = self.db.read_txn()?; + TxAndIterator::make(tx, |tx| Ok(tree.iter(tx)?)) + } + + fn iter_rev(&self, tree: usize) -> Result> { + let tree = self.get_tree(tree)?; + let tx = self.db.read_txn()?; + TxAndIterator::make(tx, |tx| Ok(tree.rev_iter(tx)?)) + } + + fn range<'r>( + &self, + tree: usize, + low: Bound<&'r [u8]>, + high: Bound<&'r [u8]>, + ) -> Result> { + let tree = self.get_tree(tree)?; + let tx = self.db.read_txn()?; + TxAndIterator::make(tx, |tx| Ok(tree.range(tx, &(low, high))?)) + } + fn range_rev<'r>( + &self, + tree: usize, + low: Bound<&'r [u8]>, + high: Bound<&'r [u8]>, + ) -> Result> { + let tree = self.get_tree(tree)?; + let tx = self.db.read_txn()?; + TxAndIterator::make(tx, |tx| Ok(tree.rev_range(tx, &(low, high))?)) + } + + // ---- + + fn transaction(&self, f: &dyn ITxFn) -> TxResult<(), ()> { + let trees = self.trees.read().unwrap(); + let mut tx = LmdbTx { + trees: &trees.0[..], + tx: self + .db + .write_txn() + .map_err(Error::from) + .map_err(TxError::Db)?, + }; + + let res = f.try_on(&mut tx); + match res { + TxFnResult::Ok => { + tx.tx.commit().map_err(Error::from).map_err(TxError::Db)?; + Ok(()) + } + TxFnResult::Abort => { + tx.tx.abort().map_err(Error::from).map_err(TxError::Db)?; + Err(TxError::Abort(())) + } + TxFnResult::DbErr => { + tx.tx.abort().map_err(Error::from).map_err(TxError::Db)?; + Err(TxError::Db(Error( + "(this message will be discarded)".into(), + ))) + } + } + } +} + +// ---- + +struct LmdbTx<'a> { + trees: &'a [Database], + tx: RwTxn<'a, 'a>, +} + +impl<'a> LmdbTx<'a> { + fn get_tree(&self, i: usize) -> TxOpResult<&Database> { + self.trees.get(i).ok_or_else(|| { + TxOpError(Error( + "invalid tree id (it might have been openned after the transaction started)".into(), + )) + }) + } +} + +impl<'a> ITx for LmdbTx<'a> { + fn get(&self, tree: usize, key: &[u8]) -> TxOpResult> { + let tree = self.get_tree(tree)?; + match tree.get(&self.tx, key)? { + Some(v) => Ok(Some(v.to_vec())), + None => Ok(None), + } + } + fn len(&self, _tree: usize) -> TxOpResult { + unimplemented!(".len() in transaction not supported with LMDB backend") + } + + fn insert(&mut self, tree: usize, key: &[u8], value: &[u8]) -> TxOpResult> { + let tree = *self.get_tree(tree)?; + let old_val = tree.get(&self.tx, key)?.map(Vec::from); + tree.put(&mut self.tx, key, value)?; + Ok(old_val) + } + fn remove(&mut self, tree: usize, key: &[u8]) -> TxOpResult> { + let tree = *self.get_tree(tree)?; + let old_val = tree.get(&self.tx, key)?.map(Vec::from); + tree.delete(&mut self.tx, key)?; + Ok(old_val) + } + + fn iter(&self, _tree: usize) -> TxOpResult> { + unimplemented!("Iterators in transactions not supported with LMDB backend"); + } + fn iter_rev(&self, _tree: usize) -> TxOpResult> { + unimplemented!("Iterators in transactions not supported with LMDB backend"); + } + + fn range<'r>( + &self, + _tree: usize, + _low: Bound<&'r [u8]>, + _high: Bound<&'r [u8]>, + ) -> TxOpResult> { + unimplemented!("Iterators in transactions not supported with LMDB backend"); + } + fn range_rev<'r>( + &self, + _tree: usize, + _low: Bound<&'r [u8]>, + _high: Bound<&'r [u8]>, + ) -> TxOpResult> { + unimplemented!("Iterators in transactions not supported with LMDB backend"); + } +} + +// ---- + +type IteratorItem<'a> = heed::Result<( + >::DItem, + >::DItem, +)>; + +struct TxAndIterator<'a, I> +where + I: Iterator> + 'a, +{ + tx: RoTxn<'a>, + iter: Option, +} + +impl<'a, I> TxAndIterator<'a, I> +where + I: Iterator> + 'a, +{ + fn make(tx: RoTxn<'a>, iterfun: F) -> Result> + where + F: FnOnce(&'a RoTxn<'a>) -> Result, + { + let mut res = TxAndIterator { tx, iter: None }; + + let tx = unsafe { NonNull::from(&res.tx).as_ref() }; + res.iter = Some(iterfun(tx)?); + + Ok(Box::new(res)) + } +} + +impl<'a, I> Drop for TxAndIterator<'a, I> +where + I: Iterator> + 'a, +{ + fn drop(&mut self) { + drop(self.iter.take()); + } +} + +impl<'a, I> Iterator for TxAndIterator<'a, I> +where + I: Iterator> + 'a, +{ + type Item = Result<(Value, Value)>; + + fn next(&mut self) -> Option { + match self.iter.as_mut().unwrap().next() { + None => None, + Some(Err(e)) => Some(Err(e.into())), + Some(Ok((k, v))) => Some(Ok((k.to_vec(), v.to_vec()))), + } + } +} diff --git a/src/db/sled_adapter.rs b/src/db/sled_adapter.rs new file mode 100644 index 00000000..982f8d82 --- /dev/null +++ b/src/db/sled_adapter.rs @@ -0,0 +1,260 @@ +use core::ops::Bound; + +use std::cell::Cell; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use sled::transaction::{ + ConflictableTransactionError, TransactionError, Transactional, TransactionalTree, + UnabortableTransactionError, +}; + +use crate::{ + Db, Error, IDb, ITx, ITxFn, Result, TxError, TxFnResult, TxOpError, TxOpResult, TxResult, + TxValueIter, Value, ValueIter, +}; + +pub use sled; + +// -- err + +impl From for Error { + fn from(e: sled::Error) -> Error { + Error(format!("Sled: {}", e).into()) + } +} + +impl From for TxOpError { + fn from(e: sled::Error) -> TxOpError { + TxOpError(e.into()) + } +} + +// -- db + +pub struct SledDb { + db: sled::Db, + trees: RwLock<(Vec, HashMap)>, +} + +impl SledDb { + pub fn init(db: sled::Db) -> Db { + let s = Self { + db, + trees: RwLock::new((Vec::new(), HashMap::new())), + }; + Db(Arc::new(s)) + } + + fn get_tree(&self, i: usize) -> Result { + self.trees + .read() + .unwrap() + .0 + .get(i) + .cloned() + .ok_or_else(|| Error("invalid tree id".into())) + } +} + +impl IDb for SledDb { + fn engine(&self) -> String { + "Sled".into() + } + + fn open_tree(&self, name: &str) -> Result { + let mut trees = self.trees.write().unwrap(); + if let Some(i) = trees.1.get(name) { + Ok(*i) + } else { + let tree = self.db.open_tree(name)?; + let i = trees.0.len(); + trees.0.push(tree); + trees.1.insert(name.to_string(), i); + Ok(i) + } + } + + fn list_trees(&self) -> Result> { + let mut trees = vec![]; + for name in self.db.tree_names() { + let name = std::str::from_utf8(&name) + .map_err(|e| Error(format!("{}", e).into()))? + .to_string(); + if name != "__sled__default" { + trees.push(name); + } + } + Ok(trees) + } + + // ---- + + fn get(&self, tree: usize, key: &[u8]) -> Result> { + let tree = self.get_tree(tree)?; + let val = tree.get(key)?; + Ok(val.map(|x| x.to_vec())) + } + + fn len(&self, tree: usize) -> Result { + let tree = self.get_tree(tree)?; + Ok(tree.len()) + } + + fn insert(&self, tree: usize, key: &[u8], value: &[u8]) -> Result> { + let tree = self.get_tree(tree)?; + let old_val = tree.insert(key, value)?; + Ok(old_val.map(|x| x.to_vec())) + } + + fn remove(&self, tree: usize, key: &[u8]) -> Result> { + let tree = self.get_tree(tree)?; + let old_val = tree.remove(key)?; + Ok(old_val.map(|x| x.to_vec())) + } + + fn iter(&self, tree: usize) -> Result> { + let tree = self.get_tree(tree)?; + Ok(Box::new(tree.iter().map(|v| { + v.map(|(x, y)| (x.to_vec(), y.to_vec())).map_err(Into::into) + }))) + } + + fn iter_rev(&self, tree: usize) -> Result> { + let tree = self.get_tree(tree)?; + Ok(Box::new(tree.iter().rev().map(|v| { + v.map(|(x, y)| (x.to_vec(), y.to_vec())).map_err(Into::into) + }))) + } + + fn range<'r>( + &self, + tree: usize, + low: Bound<&'r [u8]>, + high: Bound<&'r [u8]>, + ) -> Result> { + let tree = self.get_tree(tree)?; + Ok(Box::new(tree.range::<&'r [u8], _>((low, high)).map(|v| { + v.map(|(x, y)| (x.to_vec(), y.to_vec())).map_err(Into::into) + }))) + } + fn range_rev<'r>( + &self, + tree: usize, + low: Bound<&'r [u8]>, + high: Bound<&'r [u8]>, + ) -> Result> { + let tree = self.get_tree(tree)?; + Ok(Box::new(tree.range::<&'r [u8], _>((low, high)).rev().map( + |v| v.map(|(x, y)| (x.to_vec(), y.to_vec())).map_err(Into::into), + ))) + } + + // ---- + + fn transaction(&self, f: &dyn ITxFn) -> TxResult<(), ()> { + let trees = self.trees.read().unwrap(); + let res = trees.0.transaction(|txtrees| { + let mut tx = SledTx { + trees: txtrees, + err: Cell::new(None), + }; + match f.try_on(&mut tx) { + TxFnResult::Ok => { + assert!(tx.err.into_inner().is_none()); + Ok(()) + } + TxFnResult::Abort => { + assert!(tx.err.into_inner().is_none()); + Err(ConflictableTransactionError::Abort(())) + } + TxFnResult::DbErr => { + let e = tx.err.into_inner().expect("No DB error"); + Err(e.into()) + } + } + }); + match res { + Ok(()) => Ok(()), + Err(TransactionError::Abort(())) => Err(TxError::Abort(())), + Err(TransactionError::Storage(s)) => Err(TxError::Db(s.into())), + } + } +} + +// ---- + +struct SledTx<'a> { + trees: &'a [TransactionalTree], + err: Cell>, +} + +impl<'a> SledTx<'a> { + fn get_tree(&self, i: usize) -> TxOpResult<&TransactionalTree> { + self.trees.get(i).ok_or_else(|| { + TxOpError(Error( + "invalid tree id (it might have been openned after the transaction started)".into(), + )) + }) + } + + fn save_error( + &self, + v: std::result::Result, + ) -> TxOpResult { + match v { + Ok(x) => Ok(x), + Err(e) => { + let txt = format!("{}", e); + self.err.set(Some(e)); + Err(TxOpError(Error(txt.into()))) + } + } + } +} + +impl<'a> ITx for SledTx<'a> { + fn get(&self, tree: usize, key: &[u8]) -> TxOpResult> { + let tree = self.get_tree(tree)?; + let tmp = self.save_error(tree.get(key))?; + Ok(tmp.map(|x| x.to_vec())) + } + fn len(&self, _tree: usize) -> TxOpResult { + unimplemented!(".len() in transaction not supported with Sled backend") + } + + fn insert(&mut self, tree: usize, key: &[u8], value: &[u8]) -> TxOpResult> { + let tree = self.get_tree(tree)?; + let old_val = self.save_error(tree.insert(key, value))?; + Ok(old_val.map(|x| x.to_vec())) + } + fn remove(&mut self, tree: usize, key: &[u8]) -> TxOpResult> { + let tree = self.get_tree(tree)?; + let old_val = self.save_error(tree.remove(key))?; + Ok(old_val.map(|x| x.to_vec())) + } + + fn iter(&self, _tree: usize) -> TxOpResult> { + unimplemented!("Iterators in transactions not supported with Sled backend"); + } + fn iter_rev(&self, _tree: usize) -> TxOpResult> { + unimplemented!("Iterators in transactions not supported with Sled backend"); + } + + fn range<'r>( + &self, + _tree: usize, + _low: Bound<&'r [u8]>, + _high: Bound<&'r [u8]>, + ) -> TxOpResult> { + unimplemented!("Iterators in transactions not supported with Sled backend"); + } + fn range_rev<'r>( + &self, + _tree: usize, + _low: Bound<&'r [u8]>, + _high: Bound<&'r [u8]>, + ) -> TxOpResult> { + unimplemented!("Iterators in transactions not supported with Sled backend"); + } +} diff --git a/src/db/sqlite_adapter.rs b/src/db/sqlite_adapter.rs new file mode 100644 index 00000000..14bf35ff --- /dev/null +++ b/src/db/sqlite_adapter.rs @@ -0,0 +1,500 @@ +use core::ops::Bound; + +use std::borrow::BorrowMut; +use std::marker::PhantomPinned; +use std::pin::Pin; +use std::ptr::NonNull; +use std::sync::{Arc, Mutex, MutexGuard}; + +use log::trace; + +use rusqlite::{params, Connection, Rows, Statement, Transaction}; + +use crate::{ + Db, Error, IDb, ITx, ITxFn, Result, TxError, TxFnResult, TxOpError, TxOpResult, TxResult, + TxValueIter, Value, ValueIter, +}; + +pub use rusqlite; + +// --- err + +impl From for Error { + fn from(e: rusqlite::Error) -> Error { + Error(format!("Sqlite: {}", e).into()) + } +} + +impl From for TxOpError { + fn from(e: rusqlite::Error) -> TxOpError { + TxOpError(e.into()) + } +} + +// -- db + +pub struct SqliteDb(Mutex); + +struct SqliteDbInner { + db: Connection, + trees: Vec, +} + +impl SqliteDb { + pub fn init(db: rusqlite::Connection) -> Db { + let s = Self(Mutex::new(SqliteDbInner { + db, + trees: Vec::new(), + })); + Db(Arc::new(s)) + } +} + +impl SqliteDbInner { + fn get_tree(&self, i: usize) -> Result<&'_ str> { + self.trees + .get(i) + .map(String::as_str) + .ok_or_else(|| Error("invalid tree id".into())) + } + + fn internal_get(&self, tree: &str, key: &[u8]) -> Result> { + let mut stmt = self + .db + .prepare(&format!("SELECT v FROM {} WHERE k = ?1", tree))?; + let mut res_iter = stmt.query([key])?; + match res_iter.next()? { + None => Ok(None), + Some(v) => Ok(Some(v.get::<_, Vec>(0)?)), + } + } +} + +impl IDb for SqliteDb { + fn engine(&self) -> String { + format!("sqlite3 v{} (using rusqlite crate)", rusqlite::version()) + } + + fn open_tree(&self, name: &str) -> Result { + let name = format!("tree_{}", name.replace(':', "_COLON_")); + let mut this = self.0.lock().unwrap(); + + if let Some(i) = this.trees.iter().position(|x| x == &name) { + Ok(i) + } else { + trace!("create table {}", name); + this.db.execute( + &format!( + "CREATE TABLE IF NOT EXISTS {} ( + k BLOB PRIMARY KEY, + v BLOB + )", + name + ), + [], + )?; + trace!("table created: {}, unlocking", name); + + let i = this.trees.len(); + this.trees.push(name.to_string()); + Ok(i) + } + } + + fn list_trees(&self) -> Result> { + let mut trees = vec![]; + + trace!("list_trees: lock db"); + let this = self.0.lock().unwrap(); + trace!("list_trees: lock acquired"); + + let mut stmt = this.db.prepare( + "SELECT name FROM sqlite_schema WHERE type = 'table' AND name LIKE 'tree_%'", + )?; + let mut rows = stmt.query([])?; + while let Some(row) = rows.next()? { + let name = row.get::<_, String>(0)?; + let name = name.replace("_COLON_", ":"); + let name = name.strip_prefix("tree_").unwrap().to_string(); + trees.push(name); + } + Ok(trees) + } + + // ---- + + fn get(&self, tree: usize, key: &[u8]) -> Result> { + trace!("get {}: lock db", tree); + let this = self.0.lock().unwrap(); + trace!("get {}: lock acquired", tree); + + let tree = this.get_tree(tree)?; + this.internal_get(tree, key) + } + + fn len(&self, tree: usize) -> Result { + trace!("len {}: lock db", tree); + let this = self.0.lock().unwrap(); + trace!("len {}: lock acquired", tree); + + let tree = this.get_tree(tree)?; + let mut stmt = this.db.prepare(&format!("SELECT COUNT(*) FROM {}", tree))?; + let mut res_iter = stmt.query([])?; + match res_iter.next()? { + None => Ok(0), + Some(v) => Ok(v.get::<_, usize>(0)?), + } + } + + fn insert(&self, tree: usize, key: &[u8], value: &[u8]) -> Result> { + trace!("insert {}: lock db", tree); + let this = self.0.lock().unwrap(); + trace!("insert {}: lock acquired", tree); + + let tree = this.get_tree(tree)?; + let old_val = this.internal_get(tree, key)?; + + let sql = match &old_val { + Some(_) => format!("UPDATE {} SET v = ?2 WHERE k = ?1", tree), + None => format!("INSERT INTO {} (k, v) VALUES (?1, ?2)", tree), + }; + let n = this.db.execute(&sql, params![key, value])?; + assert_eq!(n, 1); + + Ok(old_val) + } + + fn remove(&self, tree: usize, key: &[u8]) -> Result> { + trace!("remove {}: lock db", tree); + let this = self.0.lock().unwrap(); + trace!("remove {}: lock acquired", tree); + + let tree = this.get_tree(tree)?; + let old_val = this.internal_get(tree, key)?; + + if old_val.is_some() { + let n = this + .db + .execute(&format!("DELETE FROM {} WHERE k = ?1", tree), params![key])?; + assert_eq!(n, 1); + } + + Ok(old_val) + } + + fn iter(&self, tree: usize) -> Result> { + trace!("iter {}: lock db", tree); + let this = self.0.lock().unwrap(); + trace!("iter {}: lock acquired", tree); + + let tree = this.get_tree(tree)?; + let sql = format!("SELECT k, v FROM {} ORDER BY k ASC", tree); + DbValueIterator::make(this, &sql, []) + } + + fn iter_rev(&self, tree: usize) -> Result> { + trace!("iter_rev {}: lock db", tree); + let this = self.0.lock().unwrap(); + trace!("iter_rev {}: lock acquired", tree); + + let tree = this.get_tree(tree)?; + let sql = format!("SELECT k, v FROM {} ORDER BY k DESC", tree); + DbValueIterator::make(this, &sql, []) + } + + fn range<'r>( + &self, + tree: usize, + low: Bound<&'r [u8]>, + high: Bound<&'r [u8]>, + ) -> Result> { + trace!("range {}: lock db", tree); + let this = self.0.lock().unwrap(); + trace!("range {}: lock acquired", tree); + + let tree = this.get_tree(tree)?; + + let (bounds_sql, params) = bounds_sql(low, high); + let sql = format!("SELECT k, v FROM {} {} ORDER BY k ASC", tree, bounds_sql); + + let params = params + .iter() + .map(|x| x as &dyn rusqlite::ToSql) + .collect::>(); + + DbValueIterator::make::<&[&dyn rusqlite::ToSql]>(this, &sql, params.as_ref()) + } + fn range_rev<'r>( + &self, + tree: usize, + low: Bound<&'r [u8]>, + high: Bound<&'r [u8]>, + ) -> Result> { + trace!("range_rev {}: lock db", tree); + let this = self.0.lock().unwrap(); + trace!("range_rev {}: lock acquired", tree); + + let tree = this.get_tree(tree)?; + + let (bounds_sql, params) = bounds_sql(low, high); + let sql = format!("SELECT k, v FROM {} {} ORDER BY k DESC", tree, bounds_sql); + + let params = params + .iter() + .map(|x| x as &dyn rusqlite::ToSql) + .collect::>(); + + DbValueIterator::make::<&[&dyn rusqlite::ToSql]>(this, &sql, params.as_ref()) + } + + // ---- + + fn transaction(&self, f: &dyn ITxFn) -> TxResult<(), ()> { + trace!("transaction: lock db"); + let mut this = self.0.lock().unwrap(); + trace!("transaction: lock acquired"); + + let this_mut_ref: &mut SqliteDbInner = this.borrow_mut(); + + let mut tx = SqliteTx { + tx: this_mut_ref + .db + .transaction() + .map_err(Error::from) + .map_err(TxError::Db)?, + trees: &this_mut_ref.trees, + }; + let res = match f.try_on(&mut tx) { + TxFnResult::Ok => { + tx.tx.commit().map_err(Error::from).map_err(TxError::Db)?; + Ok(()) + } + TxFnResult::Abort => { + tx.tx.rollback().map_err(Error::from).map_err(TxError::Db)?; + Err(TxError::Abort(())) + } + TxFnResult::DbErr => { + tx.tx.rollback().map_err(Error::from).map_err(TxError::Db)?; + Err(TxError::Db(Error( + "(this message will be discarded)".into(), + ))) + } + }; + + trace!("transaction done"); + res + } +} + +// ---- + +struct SqliteTx<'a> { + tx: Transaction<'a>, + trees: &'a [String], +} + +impl<'a> SqliteTx<'a> { + fn get_tree(&self, i: usize) -> TxOpResult<&'_ str> { + self.trees.get(i).map(String::as_ref).ok_or_else(|| { + TxOpError(Error( + "invalid tree id (it might have been openned after the transaction started)".into(), + )) + }) + } + + fn internal_get(&self, tree: &str, key: &[u8]) -> TxOpResult> { + let mut stmt = self + .tx + .prepare(&format!("SELECT v FROM {} WHERE k = ?1", tree))?; + let mut res_iter = stmt.query([key])?; + match res_iter.next()? { + None => Ok(None), + Some(v) => Ok(Some(v.get::<_, Vec>(0)?)), + } + } +} + +impl<'a> ITx for SqliteTx<'a> { + fn get(&self, tree: usize, key: &[u8]) -> TxOpResult> { + let tree = self.get_tree(tree)?; + self.internal_get(tree, key) + } + fn len(&self, tree: usize) -> TxOpResult { + let tree = self.get_tree(tree)?; + let mut stmt = self.tx.prepare(&format!("SELECT COUNT(*) FROM {}", tree))?; + let mut res_iter = stmt.query([])?; + match res_iter.next()? { + None => Ok(0), + Some(v) => Ok(v.get::<_, usize>(0)?), + } + } + + fn insert(&mut self, tree: usize, key: &[u8], value: &[u8]) -> TxOpResult> { + let tree = self.get_tree(tree)?; + let old_val = self.internal_get(tree, key)?; + + let sql = match &old_val { + Some(_) => format!("UPDATE {} SET v = ?2 WHERE k = ?1", tree), + None => format!("INSERT INTO {} (k, v) VALUES (?1, ?2)", tree), + }; + let n = self.tx.execute(&sql, params![key, value])?; + assert_eq!(n, 1); + + Ok(old_val) + } + fn remove(&mut self, tree: usize, key: &[u8]) -> TxOpResult> { + let tree = self.get_tree(tree)?; + let old_val = self.internal_get(tree, key)?; + + if old_val.is_some() { + let n = self + .tx + .execute(&format!("DELETE FROM {} WHERE k = ?1", tree), params![key])?; + assert_eq!(n, 1); + } + + Ok(old_val) + } + + fn iter(&self, _tree: usize) -> TxOpResult> { + unimplemented!(); + } + fn iter_rev(&self, _tree: usize) -> TxOpResult> { + unimplemented!(); + } + + fn range<'r>( + &self, + _tree: usize, + _low: Bound<&'r [u8]>, + _high: Bound<&'r [u8]>, + ) -> TxOpResult> { + unimplemented!(); + } + fn range_rev<'r>( + &self, + _tree: usize, + _low: Bound<&'r [u8]>, + _high: Bound<&'r [u8]>, + ) -> TxOpResult> { + unimplemented!(); + } +} + +// ---- + +struct DbValueIterator<'a> { + db: MutexGuard<'a, SqliteDbInner>, + stmt: Option>, + iter: Option>, + _pin: PhantomPinned, +} + +impl<'a> DbValueIterator<'a> { + fn make( + db: MutexGuard<'a, SqliteDbInner>, + sql: &str, + args: P, + ) -> Result> { + let res = DbValueIterator { + db, + stmt: None, + iter: None, + _pin: PhantomPinned, + }; + let mut boxed = Box::pin(res); + trace!("make iterator with sql: {}", sql); + + unsafe { + let db = NonNull::from(&boxed.db); + let stmt = db.as_ref().db.prepare(sql)?; + + let mut_ref: Pin<&mut DbValueIterator<'a>> = Pin::as_mut(&mut boxed); + Pin::get_unchecked_mut(mut_ref).stmt = Some(stmt); + + let mut stmt = NonNull::from(&boxed.stmt); + let iter = stmt.as_mut().as_mut().unwrap().query(args)?; + + let mut_ref: Pin<&mut DbValueIterator<'a>> = Pin::as_mut(&mut boxed); + Pin::get_unchecked_mut(mut_ref).iter = Some(iter); + } + + Ok(Box::new(DbValueIteratorPin(boxed))) + } +} + +impl<'a> Drop for DbValueIterator<'a> { + fn drop(&mut self) { + trace!("drop iter"); + drop(self.iter.take()); + drop(self.stmt.take()); + } +} + +struct DbValueIteratorPin<'a>(Pin>>); + +impl<'a> Iterator for DbValueIteratorPin<'a> { + type Item = Result<(Value, Value)>; + + fn next(&mut self) -> Option { + let next = unsafe { + let mut_ref: Pin<&mut DbValueIterator<'a>> = Pin::as_mut(&mut self.0); + Pin::get_unchecked_mut(mut_ref).iter.as_mut()?.next() + }; + let row = match next { + Err(e) => return Some(Err(e.into())), + Ok(None) => return None, + Ok(Some(r)) => r, + }; + let k = match row.get::<_, Vec>(0) { + Err(e) => return Some(Err(e.into())), + Ok(x) => x, + }; + let v = match row.get::<_, Vec>(1) { + Err(e) => return Some(Err(e.into())), + Ok(y) => y, + }; + Some(Ok((k, v))) + } +} + +// ---- + +fn bounds_sql<'r>(low: Bound<&'r [u8]>, high: Bound<&'r [u8]>) -> (String, Vec>) { + let mut sql = String::new(); + let mut params: Vec> = vec![]; + + match low { + Bound::Included(b) => { + sql.push_str(" WHERE k >= ?1"); + params.push(b.to_vec()); + } + Bound::Excluded(b) => { + sql.push_str(" WHERE k > ?1"); + params.push(b.to_vec()); + } + Bound::Unbounded => (), + }; + + match high { + Bound::Included(b) => { + if !params.is_empty() { + sql.push_str(" AND k <= ?2"); + } else { + sql.push_str(" WHERE k <= ?1"); + } + params.push(b.to_vec()); + } + Bound::Excluded(b) => { + if !params.is_empty() { + sql.push_str(" AND k < ?2"); + } else { + sql.push_str(" WHERE k < ?1"); + } + params.push(b.to_vec()); + } + Bound::Unbounded => (), + } + + (sql, params) +} diff --git a/src/db/test.rs b/src/db/test.rs new file mode 100644 index 00000000..cfcee643 --- /dev/null +++ b/src/db/test.rs @@ -0,0 +1,106 @@ +use crate::*; + +use crate::lmdb_adapter::LmdbDb; +use crate::sled_adapter::SledDb; +use crate::sqlite_adapter::SqliteDb; + +fn test_suite(db: Db) { + let tree = db.open_tree("tree").unwrap(); + + let ka: &[u8] = &b"test"[..]; + let kb: &[u8] = &b"zwello"[..]; + let kint: &[u8] = &b"tz"[..]; + let va: &[u8] = &b"plop"[..]; + let vb: &[u8] = &b"plip"[..]; + let vc: &[u8] = &b"plup"[..]; + + assert!(tree.insert(ka, va).unwrap().is_none()); + assert_eq!(tree.get(ka).unwrap().unwrap(), va); + + let res = db.transaction::<_, (), _>(|mut tx| { + assert_eq!(tx.get(&tree, ka).unwrap().unwrap(), va); + + assert_eq!(tx.insert(&tree, ka, vb).unwrap().unwrap(), va); + + assert_eq!(tx.get(&tree, ka).unwrap().unwrap(), vb); + + tx.commit(12) + }); + assert!(matches!(res, Ok(12))); + assert_eq!(tree.get(ka).unwrap().unwrap(), vb); + + let res = db.transaction::<(), _, _>(|mut tx| { + assert_eq!(tx.get(&tree, ka).unwrap().unwrap(), vb); + + assert_eq!(tx.insert(&tree, ka, vc).unwrap().unwrap(), vb); + + assert_eq!(tx.get(&tree, ka).unwrap().unwrap(), vc); + + tx.abort(42) + }); + assert!(matches!(res, Err(TxError::Abort(42)))); + assert_eq!(tree.get(ka).unwrap().unwrap(), vb); + + let mut iter = tree.iter().unwrap(); + let next = iter.next().unwrap().unwrap(); + assert_eq!((next.0.as_ref(), next.1.as_ref()), (ka, vb)); + assert!(iter.next().is_none()); + drop(iter); + + assert!(tree.insert(kb, vc).unwrap().is_none()); + assert_eq!(tree.get(kb).unwrap().unwrap(), vc); + + let mut iter = tree.iter().unwrap(); + let next = iter.next().unwrap().unwrap(); + assert_eq!((next.0.as_ref(), next.1.as_ref()), (ka, vb)); + let next = iter.next().unwrap().unwrap(); + assert_eq!((next.0.as_ref(), next.1.as_ref()), (kb, vc)); + assert!(iter.next().is_none()); + drop(iter); + + let mut iter = tree.range(kint..).unwrap(); + let next = iter.next().unwrap().unwrap(); + assert_eq!((next.0.as_ref(), next.1.as_ref()), (kb, vc)); + assert!(iter.next().is_none()); + drop(iter); + + let mut iter = tree.range_rev(..kint).unwrap(); + let next = iter.next().unwrap().unwrap(); + assert_eq!((next.0.as_ref(), next.1.as_ref()), (ka, vb)); + assert!(iter.next().is_none()); + drop(iter); + + let mut iter = tree.iter_rev().unwrap(); + let next = iter.next().unwrap().unwrap(); + assert_eq!((next.0.as_ref(), next.1.as_ref()), (kb, vc)); + let next = iter.next().unwrap().unwrap(); + assert_eq!((next.0.as_ref(), next.1.as_ref()), (ka, vb)); + assert!(iter.next().is_none()); + drop(iter); +} + +#[test] +fn test_lmdb_db() { + let path = mktemp::Temp::new_dir().unwrap(); + let db = heed::EnvOpenOptions::new() + .max_dbs(100) + .open(&path) + .unwrap(); + let db = LmdbDb::init(db); + test_suite(db); + drop(path); +} + +#[test] +fn test_sled_db() { + let path = mktemp::Temp::new_dir().unwrap(); + let db = SledDb::init(sled::open(path.to_path_buf()).unwrap()); + test_suite(db); + drop(path); +} + +#[test] +fn test_sqlite_db() { + let db = SqliteDb::init(rusqlite::Connection::open_in_memory().unwrap()); + test_suite(db); +} diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index 902f67f8..eb643160 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -21,6 +21,7 @@ path = "tests/lib.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +garage_db = { version = "0.8.0", path = "../db" } garage_api = { version = "0.7.0", path = "../api" } garage_model = { version = "0.7.0", path = "../model" } garage_rpc = { version = "0.7.0", path = "../rpc" } @@ -36,8 +37,6 @@ rand = "0.8" async-trait = "0.1.7" sodiumoxide = { version = "0.2.5-0", package = "kuska-sodiumoxide" } -sled = "0.34" - rmp-serde = "0.15" serde = { version = "1.0", default-features = false, features = ["derive", "rc"] } serde_bytes = "0.11" diff --git a/src/garage/admin.rs b/src/garage/admin.rs index bc1f494a..c662aa00 100644 --- a/src/garage/admin.rs +++ b/src/garage/admin.rs @@ -660,11 +660,11 @@ impl AdminRpcHandler { } Ok(AdminRpc::Ok(ret)) } else { - Ok(AdminRpc::Ok(self.gather_stats_local(opt))) + Ok(AdminRpc::Ok(self.gather_stats_local(opt)?)) } } - fn gather_stats_local(&self, opt: StatsOpt) -> String { + fn gather_stats_local(&self, opt: StatsOpt) -> Result { let mut ret = String::new(); writeln!( &mut ret, @@ -672,6 +672,7 @@ impl AdminRpcHandler { self.garage.system.garage_version(), ) .unwrap(); + writeln!(&mut ret, "\nDatabase engine: {}", self.garage.db.engine()).unwrap(); // Gather ring statistics let ring = self.garage.system.ring.borrow().clone(); @@ -689,59 +690,71 @@ impl AdminRpcHandler { writeln!(&mut ret, " {:?} {}", n, c).unwrap(); } - self.gather_table_stats(&mut ret, &self.garage.bucket_table, &opt); - self.gather_table_stats(&mut ret, &self.garage.key_table, &opt); - self.gather_table_stats(&mut ret, &self.garage.object_table, &opt); - self.gather_table_stats(&mut ret, &self.garage.version_table, &opt); - self.gather_table_stats(&mut ret, &self.garage.block_ref_table, &opt); + self.gather_table_stats(&mut ret, &self.garage.bucket_table, &opt)?; + self.gather_table_stats(&mut ret, &self.garage.key_table, &opt)?; + self.gather_table_stats(&mut ret, &self.garage.object_table, &opt)?; + self.gather_table_stats(&mut ret, &self.garage.version_table, &opt)?; + self.gather_table_stats(&mut ret, &self.garage.block_ref_table, &opt)?; writeln!(&mut ret, "\nBlock manager stats:").unwrap(); if opt.detailed { writeln!( &mut ret, " number of RC entries (~= number of blocks): {}", - self.garage.block_manager.rc_len() + self.garage.block_manager.rc_len()? ) .unwrap(); } writeln!( &mut ret, " resync queue length: {}", - self.garage.block_manager.resync_queue_len() + self.garage.block_manager.resync_queue_len()? ) .unwrap(); writeln!( &mut ret, " blocks with resync errors: {}", - self.garage.block_manager.resync_errors_len() + self.garage.block_manager.resync_errors_len()? ) .unwrap(); - ret + Ok(ret) } - fn gather_table_stats(&self, to: &mut String, t: &Arc>, opt: &StatsOpt) + fn gather_table_stats( + &self, + to: &mut String, + t: &Arc>, + opt: &StatsOpt, + ) -> Result<(), Error> where F: TableSchema + 'static, R: TableReplication + 'static, { writeln!(to, "\nTable stats for {}", F::TABLE_NAME).unwrap(); if opt.detailed { - writeln!(to, " number of items: {}", t.data.store.len()).unwrap(); + writeln!( + to, + " number of items: {}", + t.data.store.len().map_err(GarageError::from)? + ) + .unwrap(); writeln!( to, " Merkle tree size: {}", - t.merkle_updater.merkle_tree_len() + t.merkle_updater.merkle_tree_len()? ) .unwrap(); } writeln!( to, " Merkle updater todo queue length: {}", - t.merkle_updater.todo_len() + t.merkle_updater.todo_len()? ) .unwrap(); - writeln!(to, " GC todo queue length: {}", t.data.gc_todo_len()).unwrap(); + writeln!(to, " GC todo queue length: {}", t.data.gc_todo_len()?).unwrap(); + + Ok(()) } } diff --git a/src/garage/repair.rs b/src/garage/repair.rs index 830eac71..17e14b8b 100644 --- a/src/garage/repair.rs +++ b/src/garage/repair.rs @@ -64,13 +64,23 @@ impl Repair { async fn repair_versions(&self, must_exit: &watch::Receiver) -> Result<(), Error> { let mut pos = vec![]; + let mut i = 0; - while let Some((item_key, item_bytes)) = - self.garage.version_table.data.store.get_gt(&pos)? - { - pos = item_key.to_vec(); + while !*must_exit.borrow() { + let item_bytes = match self.garage.version_table.data.store.get_gt(pos)? { + Some((k, v)) => { + pos = k; + v + } + None => break, + }; - let version = rmp_serde::decode::from_read_ref::<_, Version>(item_bytes.as_ref())?; + i += 1; + if i % 1000 == 0 { + info!("repair_versions: {}", i); + } + + let version = rmp_serde::decode::from_read_ref::<_, Version>(&item_bytes)?; if version.deleted.get() { continue; } @@ -98,23 +108,30 @@ impl Repair { )) .await?; } - - if *must_exit.borrow() { - break; - } } + info!("repair_versions: finished, done {}", i); Ok(()) } async fn repair_block_ref(&self, must_exit: &watch::Receiver) -> Result<(), Error> { let mut pos = vec![]; + let mut i = 0; - while let Some((item_key, item_bytes)) = - self.garage.block_ref_table.data.store.get_gt(&pos)? - { - pos = item_key.to_vec(); + while !*must_exit.borrow() { + let item_bytes = match self.garage.block_ref_table.data.store.get_gt(pos)? { + Some((k, v)) => { + pos = k; + v + } + None => break, + }; - let block_ref = rmp_serde::decode::from_read_ref::<_, BlockRef>(item_bytes.as_ref())?; + i += 1; + if i % 1000 == 0 { + info!("repair_block_ref: {}", i); + } + + let block_ref = rmp_serde::decode::from_read_ref::<_, BlockRef>(&item_bytes)?; if block_ref.deleted.get() { continue; } @@ -139,11 +156,8 @@ impl Repair { }) .await?; } - - if *must_exit.borrow() { - break; - } } + info!("repair_block_ref: finished, done {}", i); Ok(()) } } diff --git a/src/garage/server.rs b/src/garage/server.rs index b58ad286..697d3358 100644 --- a/src/garage/server.rs +++ b/src/garage/server.rs @@ -2,6 +2,8 @@ use std::path::PathBuf; use tokio::sync::watch; +use garage_db as db; + use garage_util::background::*; use garage_util::config::*; use garage_util::error::Error; @@ -31,13 +33,51 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> { info!("Opening database..."); let mut db_path = config.metadata_dir.clone(); - db_path.push("db"); - let db = sled::Config::default() - .path(&db_path) - .cache_capacity(config.sled_cache_capacity) - .flush_every_ms(Some(config.sled_flush_every_ms)) - .open() - .expect("Unable to open sled DB"); + std::fs::create_dir_all(&db_path).expect("Unable to create Garage meta data directory"); + let db = match config.db_engine.as_str() { + "sled" => { + db_path.push("db"); + info!("Opening Sled database at: {}", db_path.display()); + let db = db::sled_adapter::sled::Config::default() + .path(&db_path) + .cache_capacity(config.sled_cache_capacity) + .flush_every_ms(Some(config.sled_flush_every_ms)) + .open() + .expect("Unable to open sled DB"); + db::sled_adapter::SledDb::init(db) + } + "sqlite" | "sqlite3" | "rusqlite" => { + db_path.push("db.sqlite"); + info!("Opening Sqlite database at: {}", db_path.display()); + let db = db::sqlite_adapter::rusqlite::Connection::open(db_path) + .expect("Unable to open sqlite DB"); + db::sqlite_adapter::SqliteDb::init(db) + } + "lmdb" | "heed" => { + db_path.push("db.lmdb"); + info!("Opening LMDB database at: {}", db_path.display()); + std::fs::create_dir_all(&db_path).expect("Unable to create LMDB data directory"); + let map_size = if u32::MAX as usize == usize::MAX { + warn!("LMDB is not recommended on 32-bit systems, database size will be limited"); + 1usize << 30 // 1GB for 32-bit systems + } else { + 1usize << 40 // 1TB for 64-bit systems + }; + + let db = db::lmdb_adapter::heed::EnvOpenOptions::new() + .max_dbs(100) + .map_size(map_size) + .open(&db_path) + .expect("Unable to open LMDB DB"); + db::lmdb_adapter::LmdbDb::init(db) + } + e => { + return Err(Error::Message(format!( + "Unsupported DB engine: {} (options: sled, sqlite, lmdb)", + e + ))); + } + }; info!("Initializing background runner..."); let watch_cancel = netapp::util::watch_ctrl_c(); diff --git a/src/garage/tests/bucket.rs b/src/garage/tests/bucket.rs index ff5cc8da..b32af068 100644 --- a/src/garage/tests/bucket.rs +++ b/src/garage/tests/bucket.rs @@ -29,8 +29,7 @@ async fn test_bucket_all() { .unwrap() .iter() .filter(|x| x.name.as_ref().is_some()) - .find(|x| x.name.as_ref().unwrap() == "hello") - .is_some()); + .any(|x| x.name.as_ref().unwrap() == "hello")); } { // Get its location @@ -75,13 +74,12 @@ async fn test_bucket_all() { { // Check bucket is deleted with List buckets let r = ctx.client.list_buckets().send().await.unwrap(); - assert!(r + assert!(!r .buckets .as_ref() .unwrap() .iter() .filter(|x| x.name.as_ref().is_some()) - .find(|x| x.name.as_ref().unwrap() == "hello") - .is_none()); + .any(|x| x.name.as_ref().unwrap() == "hello")); } } diff --git a/src/model/Cargo.toml b/src/model/Cargo.toml index 133fe44e..d908dc01 100644 --- a/src/model/Cargo.toml +++ b/src/model/Cargo.toml @@ -14,6 +14,7 @@ path = "lib.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +garage_db = { version = "0.8.0", path = "../db" } garage_rpc = { version = "0.7.0", path = "../rpc" } garage_table = { version = "0.7.0", path = "../table" } garage_block = { version = "0.7.0", path = "../block" } @@ -30,8 +31,6 @@ tracing = "0.1.30" rand = "0.8" zstd = { version = "0.9", default-features = false } -sled = "0.34" - rmp-serde = "0.15" serde = { version = "1.0", default-features = false, features = ["derive", "rc"] } serde_bytes = "0.11" diff --git a/src/model/garage.rs b/src/model/garage.rs index 2f99bd68..280f3dc7 100644 --- a/src/model/garage.rs +++ b/src/model/garage.rs @@ -2,6 +2,8 @@ use std::sync::Arc; use netapp::NetworkKey; +use garage_db as db; + use garage_util::background::*; use garage_util::config::*; @@ -33,7 +35,7 @@ pub struct Garage { pub config: Config, /// The local database - pub db: sled::Db, + pub db: db::Db, /// A background job runner pub background: Arc, /// The membership manager @@ -71,7 +73,7 @@ pub struct GarageK2V { impl Garage { /// Create and run garage - pub fn new(config: Config, db: sled::Db, background: Arc) -> Arc { + pub fn new(config: Config, db: db::Db, background: Arc) -> Arc { let network_key = NetworkKey::from_slice( &hex::decode(&config.rpc_secret).expect("Invalid RPC secret key")[..], ) @@ -199,7 +201,7 @@ impl Garage { #[cfg(feature = "k2v")] impl GarageK2V { - fn new(system: Arc, db: &sled::Db, meta_rep_param: TableShardedReplication) -> Self { + fn new(system: Arc, db: &db::Db, meta_rep_param: TableShardedReplication) -> Self { info!("Initialize K2V counter table..."); let counter_table = IndexCounter::new(system.clone(), meta_rep_param.clone(), db); info!("Initialize K2V subscription manager..."); diff --git a/src/model/index_counter.rs b/src/model/index_counter.rs index 123154d4..2602d5d9 100644 --- a/src/model/index_counter.rs +++ b/src/model/index_counter.rs @@ -6,6 +6,8 @@ use std::time::Duration; use serde::{Deserialize, Serialize}; use tokio::sync::{mpsc, watch}; +use garage_db as db; + use garage_rpc::ring::Ring; use garage_rpc::system::System; use garage_util::data::*; @@ -114,10 +116,6 @@ impl TableSchema for CounterTable { type E = CounterEntry; type Filter = (DeletedFilter, Vec); - fn updated(&self, _old: Option<&Self::E>, _new: Option<&Self::E>) { - // nothing for now - } - fn matches_filter(entry: &Self::E, filter: &Self::Filter) -> bool { if filter.0 == DeletedFilter::Any { return true; @@ -135,7 +133,7 @@ impl TableSchema for CounterTable { pub struct IndexCounter { this_node: Uuid, - local_counter: sled::Tree, + local_counter: db::Tree, propagate_tx: mpsc::UnboundedSender<(T::P, T::S, LocalCounterEntry)>, pub table: Arc, TableShardedReplication>>, } @@ -144,7 +142,7 @@ impl IndexCounter { pub fn new( system: Arc, replication: TableShardedReplication, - db: &sled::Db, + db: &db::Db, ) -> Arc { let background = system.background.clone(); @@ -174,36 +172,36 @@ impl IndexCounter { this } - pub fn count(&self, pk: &T::P, sk: &T::S, counts: &[(&str, i64)]) -> Result<(), Error> { + pub fn count( + &self, + tx: &mut db::Transaction, + pk: &T::P, + sk: &T::S, + counts: &[(&str, i64)], + ) -> db::TxResult<(), Error> { let tree_key = self.table.data.tree_key(pk, sk); - let new_entry = self.local_counter.transaction(|tx| { - let mut entry = match tx.get(&tree_key[..])? { - Some(old_bytes) => { - rmp_serde::decode::from_read_ref::<_, LocalCounterEntry>(&old_bytes) - .map_err(Error::RmpDecode) - .map_err(sled::transaction::ConflictableTransactionError::Abort)? - } - None => LocalCounterEntry { - values: BTreeMap::new(), - }, - }; + let mut entry = match tx.get(&self.local_counter, &tree_key[..])? { + Some(old_bytes) => rmp_serde::decode::from_read_ref::<_, LocalCounterEntry>(&old_bytes) + .map_err(Error::RmpDecode) + .map_err(db::TxError::Abort)?, + None => LocalCounterEntry { + values: BTreeMap::new(), + }, + }; - for (s, inc) in counts.iter() { - let mut ent = entry.values.entry(s.to_string()).or_insert((0, 0)); - ent.0 += 1; - ent.1 += *inc; - } + for (s, inc) in counts.iter() { + let mut ent = entry.values.entry(s.to_string()).or_insert((0, 0)); + ent.0 += 1; + ent.1 += *inc; + } - let new_entry_bytes = rmp_to_vec_all_named(&entry) - .map_err(Error::RmpEncode) - .map_err(sled::transaction::ConflictableTransactionError::Abort)?; - tx.insert(&tree_key[..], new_entry_bytes)?; + let new_entry_bytes = rmp_to_vec_all_named(&entry) + .map_err(Error::RmpEncode) + .map_err(db::TxError::Abort)?; + tx.insert(&self.local_counter, &tree_key[..], new_entry_bytes)?; - Ok(entry) - })?; - - if let Err(e) = self.propagate_tx.send((pk.clone(), sk.clone(), new_entry)) { + if let Err(e) = self.propagate_tx.send((pk.clone(), sk.clone(), entry)) { error!( "Could not propagate updated counter values, failed to send to channel: {}", e diff --git a/src/model/k2v/item_table.rs b/src/model/k2v/item_table.rs index 8b7cc08a..991fe66d 100644 --- a/src/model/k2v/item_table.rs +++ b/src/model/k2v/item_table.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::sync::Arc; +use garage_db as db; use garage_util::data::*; use garage_table::crdt::*; @@ -221,7 +222,12 @@ impl TableSchema for K2VItemTable { type E = K2VItem; type Filter = ItemFilter; - fn updated(&self, old: Option<&Self::E>, new: Option<&Self::E>) { + fn updated( + &self, + tx: &mut db::Transaction, + old: Option<&Self::E>, + new: Option<&Self::E>, + ) -> db::TxOpResult<()> { // 1. Count let (old_entries, old_conflicts, old_values, old_bytes) = match old { None => (0, 0, 0, 0), @@ -239,7 +245,8 @@ impl TableSchema for K2VItemTable { .map(|e| &e.partition.partition_key) .unwrap_or_else(|| &new.unwrap().partition.partition_key); - if let Err(e) = self.counter_table.count( + let counter_res = self.counter_table.count( + tx, &count_pk, count_sk, &[ @@ -248,14 +255,23 @@ impl TableSchema for K2VItemTable { (VALUES, new_values - old_values), (BYTES, new_bytes - old_bytes), ], - ) { - error!("Could not update K2V counter for bucket {:?} partition {}; counts will now be inconsistent. {}", count_pk, count_sk, e); + ); + if let Err(e) = db::unabort(counter_res)? { + // This result can be returned by `counter_table.count()` for instance + // if messagepack serialization or deserialization fails at some step. + // Warn admin but ignore this error for now, that's all we can do. + error!( + "Unable to update K2V item counter for bucket {:?} partition {}: {}. Index values will be wrong!", + count_pk, count_sk, e + ); } // 2. Notify if let Some(new_ent) = new { self.subscriptions.notify(new_ent); } + + Ok(()) } #[allow(clippy::nonminimal_bool)] diff --git a/src/model/migrate.rs b/src/model/migrate.rs index 7e61957a..25acb4b0 100644 --- a/src/model/migrate.rs +++ b/src/model/migrate.rs @@ -25,11 +25,15 @@ impl Migrate { .open_tree("bucket:table") .map_err(GarageError::from)?; - for res in tree.iter() { + let mut old_buckets = vec![]; + for res in tree.iter().map_err(GarageError::from)? { let (_k, v) = res.map_err(GarageError::from)?; let bucket = rmp_serde::decode::from_read_ref::<_, old_bucket::Bucket>(&v[..]) .map_err(GarageError::from)?; + old_buckets.push(bucket); + } + for bucket in old_buckets { if let old_bucket::BucketState::Present(p) = bucket.state.get() { self.migrate_buckets050_do_bucket(&bucket, p).await?; } diff --git a/src/model/s3/block_ref_table.rs b/src/model/s3/block_ref_table.rs index 9b3991bf..9589b4aa 100644 --- a/src/model/s3/block_ref_table.rs +++ b/src/model/s3/block_ref_table.rs @@ -1,6 +1,8 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; +use garage_db as db; + use garage_util::data::*; use garage_table::crdt::Crdt; @@ -51,21 +53,22 @@ impl TableSchema for BlockRefTable { type E = BlockRef; type Filter = DeletedFilter; - fn updated(&self, old: Option<&Self::E>, new: Option<&Self::E>) { - #[allow(clippy::or_fun_call)] - let block = &old.or(new).unwrap().block; + fn updated( + &self, + tx: &mut db::Transaction, + old: Option<&Self::E>, + new: Option<&Self::E>, + ) -> db::TxOpResult<()> { + let block = old.or(new).unwrap().block; let was_before = old.map(|x| !x.deleted.get()).unwrap_or(false); let is_after = new.map(|x| !x.deleted.get()).unwrap_or(false); if is_after && !was_before { - if let Err(e) = self.block_manager.block_incref(block) { - warn!("block_incref failed for block {:?}: {}", block, e); - } + self.block_manager.block_incref(tx, block)?; } if was_before && !is_after { - if let Err(e) = self.block_manager.block_decref(block) { - warn!("block_decref failed for block {:?}: {}", block, e); - } + self.block_manager.block_decref(tx, block)?; } + Ok(()) } fn matches_filter(entry: &Self::E, filter: &Self::Filter) -> bool { diff --git a/src/model/s3/object_table.rs b/src/model/s3/object_table.rs index 3d9a89f7..62f5d8d9 100644 --- a/src/model/s3/object_table.rs +++ b/src/model/s3/object_table.rs @@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::sync::Arc; +use garage_db as db; + use garage_util::background::BackgroundRunner; use garage_util::data::*; @@ -232,7 +234,12 @@ impl TableSchema for ObjectTable { type E = Object; type Filter = ObjectFilter; - fn updated(&self, old: Option<&Self::E>, new: Option<&Self::E>) { + fn updated( + &self, + _tx: &mut db::Transaction, + old: Option<&Self::E>, + new: Option<&Self::E>, + ) -> db::TxOpResult<()> { let version_table = self.version_table.clone(); let old = old.cloned(); let new = new.cloned(); @@ -259,7 +266,8 @@ impl TableSchema for ObjectTable { } } Ok(()) - }) + }); + Ok(()) } fn matches_filter(entry: &Self::E, filter: &Self::Filter) -> bool { diff --git a/src/model/s3/version_table.rs b/src/model/s3/version_table.rs index ad096772..881c245a 100644 --- a/src/model/s3/version_table.rs +++ b/src/model/s3/version_table.rs @@ -1,6 +1,8 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; +use garage_db as db; + use garage_util::background::BackgroundRunner; use garage_util::data::*; @@ -137,7 +139,12 @@ impl TableSchema for VersionTable { type E = Version; type Filter = DeletedFilter; - fn updated(&self, old: Option<&Self::E>, new: Option<&Self::E>) { + fn updated( + &self, + _tx: &mut db::Transaction, + old: Option<&Self::E>, + new: Option<&Self::E>, + ) -> db::TxOpResult<()> { let block_ref_table = self.block_ref_table.clone(); let old = old.cloned(); let new = new.cloned(); @@ -160,7 +167,9 @@ impl TableSchema for VersionTable { } } Ok(()) - }) + }); + + Ok(()) } fn matches_filter(entry: &Self::E, filter: &Self::Filter) -> bool { diff --git a/src/table/Cargo.toml b/src/table/Cargo.toml index ed1a213f..6de37cda 100644 --- a/src/table/Cargo.toml +++ b/src/table/Cargo.toml @@ -14,6 +14,7 @@ path = "lib.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +garage_db = { version = "0.8.0", path = "../db" } garage_rpc = { version = "0.7.0", path = "../rpc" } garage_util = { version = "0.7.0", path = "../util" } @@ -25,8 +26,6 @@ hexdump = "0.1" tracing = "0.1.30" rand = "0.8" -sled = "0.34" - rmp-serde = "0.15" serde = { version = "1.0", default-features = false, features = ["derive", "rc"] } serde_bytes = "0.11" diff --git a/src/table/data.rs b/src/table/data.rs index 5cb10066..3212e82b 100644 --- a/src/table/data.rs +++ b/src/table/data.rs @@ -3,12 +3,13 @@ use std::convert::TryInto; use std::sync::Arc; use serde_bytes::ByteBuf; -use sled::{IVec, Transactional}; use tokio::sync::Notify; +use garage_db as db; +use garage_db::counted_tree_hack::CountedTree; + use garage_util::data::*; use garage_util::error::*; -use garage_util::sled_counter::SledCountedTree; use garage_rpc::system::System; @@ -25,12 +26,12 @@ pub struct TableData { pub instance: F, pub replication: R, - pub store: sled::Tree, + pub store: db::Tree, - pub(crate) merkle_tree: sled::Tree, - pub(crate) merkle_todo: sled::Tree, + pub(crate) merkle_tree: db::Tree, + pub(crate) merkle_todo: db::Tree, pub(crate) merkle_todo_notify: Notify, - pub(crate) gc_todo: SledCountedTree, + pub(crate) gc_todo: CountedTree, pub(crate) metrics: TableMetrics, } @@ -40,7 +41,7 @@ where F: TableSchema, R: TableReplication, { - pub fn new(system: Arc, instance: F, replication: R, db: &sled::Db) -> Arc { + pub fn new(system: Arc, instance: F, replication: R, db: &db::Db) -> Arc { let store = db .open_tree(&format!("{}:table", F::TABLE_NAME)) .expect("Unable to open DB tree"); @@ -55,7 +56,7 @@ where let gc_todo = db .open_tree(&format!("{}:gc_todo_v2", F::TABLE_NAME)) .expect("Unable to open DB tree"); - let gc_todo = SledCountedTree::new(gc_todo); + let gc_todo = CountedTree::new(gc_todo).expect("Cannot count gc_todo_v2"); let metrics = TableMetrics::new(F::TABLE_NAME, merkle_todo.clone(), gc_todo.clone()); @@ -98,30 +99,30 @@ where None => partition_hash.to_vec(), Some(sk) => self.tree_key(partition_key, sk), }; - let range = self.store.range(first_key..); + let range = self.store.range(first_key..)?; self.read_range_aux(partition_hash, range, filter, limit) } EnumerationOrder::Reverse => match start { Some(sk) => { let last_key = self.tree_key(partition_key, sk); - let range = self.store.range(..=last_key).rev(); + let range = self.store.range_rev(..=last_key)?; self.read_range_aux(partition_hash, range, filter, limit) } None => { let mut last_key = partition_hash.to_vec(); let lower = u128::from_be_bytes(last_key[16..32].try_into().unwrap()); last_key[16..32].copy_from_slice(&u128::to_be_bytes(lower + 1)); - let range = self.store.range(..last_key).rev(); + let range = self.store.range_rev(..last_key)?; self.read_range_aux(partition_hash, range, filter, limit) } }, } } - fn read_range_aux( + fn read_range_aux<'a>( &self, partition_hash: Hash, - range: impl Iterator>, + range: db::ValueIter<'a>, filter: &Option, limit: usize, ) -> Result>, Error> { @@ -139,7 +140,7 @@ where } }; if keep { - ret.push(Arc::new(ByteBuf::from(value.as_ref()))); + ret.push(Arc::new(ByteBuf::from(value))); } if ret.len() >= limit { break; @@ -183,12 +184,10 @@ where tree_key: &[u8], f: impl Fn(Option) -> F::E, ) -> Result, Error> { - let changed = (&self.store, &self.merkle_todo).transaction(|(store, mkl_todo)| { - let (old_entry, old_bytes, new_entry) = match store.get(tree_key)? { + let changed = self.store.db().transaction(|mut tx| { + let (old_entry, old_bytes, new_entry) = match tx.get(&self.store, tree_key)? { Some(old_bytes) => { - let old_entry = self - .decode_entry(&old_bytes) - .map_err(sled::transaction::ConflictableTransactionError::Abort)?; + let old_entry = self.decode_entry(&old_bytes).map_err(db::TxError::Abort)?; let new_entry = f(Some(old_entry.clone())); (Some(old_entry), Some(old_bytes), new_entry) } @@ -204,24 +203,28 @@ where // the associated Merkle tree entry. let new_bytes = rmp_to_vec_all_named(&new_entry) .map_err(Error::RmpEncode) - .map_err(sled::transaction::ConflictableTransactionError::Abort)?; + .map_err(db::TxError::Abort)?; let encoding_changed = Some(&new_bytes[..]) != old_bytes.as_ref().map(|x| &x[..]); + drop(old_bytes); if value_changed || encoding_changed { let new_bytes_hash = blake2sum(&new_bytes[..]); - mkl_todo.insert(tree_key.to_vec(), new_bytes_hash.as_slice())?; - store.insert(tree_key.to_vec(), new_bytes)?; - Ok(Some((old_entry, new_entry, new_bytes_hash))) + tx.insert(&self.merkle_todo, tree_key, new_bytes_hash.as_slice())?; + tx.insert(&self.store, tree_key, new_bytes)?; + + self.instance + .updated(&mut tx, old_entry.as_ref(), Some(&new_entry))?; + + Ok(Some((new_entry, new_bytes_hash))) } else { Ok(None) } })?; - if let Some((old_entry, new_entry, new_bytes_hash)) = changed { + if let Some((new_entry, new_bytes_hash)) = changed { self.metrics.internal_update_counter.add(1); let is_tombstone = new_entry.is_tombstone(); - self.instance.updated(old_entry.as_ref(), Some(&new_entry)); self.merkle_todo_notify.notify_one(); if is_tombstone { // We are only responsible for GC'ing this item if we are the @@ -244,22 +247,23 @@ where } pub(crate) fn delete_if_equal(self: &Arc, k: &[u8], v: &[u8]) -> Result { - let removed = (&self.store, &self.merkle_todo).transaction(|(store, mkl_todo)| { - if let Some(cur_v) = store.get(k)? { - if cur_v == v { - store.remove(k)?; - mkl_todo.insert(k, vec![])?; - return Ok(true); + let removed = self + .store + .db() + .transaction(|mut tx| match tx.get(&self.store, k)? { + Some(cur_v) if cur_v == v => { + tx.remove(&self.store, k)?; + tx.insert(&self.merkle_todo, k, vec![])?; + + let old_entry = self.decode_entry(v).map_err(db::TxError::Abort)?; + self.instance.updated(&mut tx, Some(&old_entry), None)?; + Ok(true) } - } - Ok(false) - })?; + _ => Ok(false), + })?; if removed { self.metrics.internal_delete_counter.add(1); - - let old_entry = self.decode_entry(v)?; - self.instance.updated(Some(&old_entry), None); self.merkle_todo_notify.notify_one(); } Ok(removed) @@ -270,25 +274,26 @@ where k: &[u8], vhash: Hash, ) -> Result { - let removed = (&self.store, &self.merkle_todo).transaction(|(store, mkl_todo)| { - if let Some(cur_v) = store.get(k)? { - if blake2sum(&cur_v[..]) == vhash { - store.remove(k)?; - mkl_todo.insert(k, vec![])?; - return Ok(Some(cur_v)); - } - } - Ok(None) - })?; + let removed = self + .store + .db() + .transaction(|mut tx| match tx.get(&self.store, k)? { + Some(cur_v) if blake2sum(&cur_v[..]) == vhash => { + tx.remove(&self.store, k)?; + tx.insert(&self.merkle_todo, k, vec![])?; - if let Some(old_v) = removed { - let old_entry = self.decode_entry(&old_v[..])?; - self.instance.updated(Some(&old_entry), None); + let old_entry = self.decode_entry(&cur_v[..]).map_err(db::TxError::Abort)?; + self.instance.updated(&mut tx, Some(&old_entry), None)?; + Ok(true) + } + _ => Ok(false), + })?; + + if removed { + self.metrics.internal_delete_counter.add(1); self.merkle_todo_notify.notify_one(); - Ok(true) - } else { - Ok(false) } + Ok(removed) } // ---- Utility functions ---- @@ -315,7 +320,7 @@ where } } - pub fn gc_todo_len(&self) -> usize { - self.gc_todo.len() + pub fn gc_todo_len(&self) -> Result { + Ok(self.gc_todo.len()) } } diff --git a/src/table/gc.rs b/src/table/gc.rs index 2a05b6ae..e7fbbcb0 100644 --- a/src/table/gc.rs +++ b/src/table/gc.rs @@ -12,9 +12,10 @@ use futures::select; use futures_util::future::*; use tokio::sync::watch; +use garage_db::counted_tree_hack::CountedTree; + use garage_util::data::*; use garage_util::error::*; -use garage_util::sled_counter::SledCountedTree; use garage_util::time::*; use garage_rpc::system::System; @@ -100,18 +101,16 @@ where async fn gc_loop_iter(&self) -> Result, Error> { let now = now_msec(); - let mut entries = vec![]; - let mut excluded = vec![]; - // List entries in the GC todo list // These entries are put there when a tombstone is inserted in the table // (see update_entry in data.rs) - for entry_kv in self.data.gc_todo.iter() { + let mut candidates = vec![]; + for entry_kv in self.data.gc_todo.iter()? { let (k, vhash) = entry_kv?; - let mut todo_entry = GcTodoEntry::parse(&k, &vhash); + let todo_entry = GcTodoEntry::parse(&k, &vhash); if todo_entry.deletion_time() > now { - if entries.is_empty() && excluded.is_empty() { + if candidates.is_empty() { // If the earliest entry in the todo list shouldn't yet be processed, // return a duration to wait in the loop return Ok(Some(Duration::from_millis( @@ -123,15 +122,23 @@ where } } - let vhash = Hash::try_from(&vhash[..]).unwrap(); + candidates.push(todo_entry); + if candidates.len() >= 2 * TABLE_GC_BATCH_SIZE { + break; + } + } + let mut entries = vec![]; + let mut excluded = vec![]; + for mut todo_entry in candidates { // Check if the tombstone is still the current value of the entry. // If not, we don't actually want to GC it, and we will remove it // from the gc_todo table later (below). + let vhash = todo_entry.value_hash; todo_entry.value = self .data .store - .get(&k[..])? + .get(&todo_entry.key[..])? .filter(|v| blake2sum(&v[..]) == vhash) .map(|v| v.to_vec()); @@ -353,17 +360,17 @@ impl GcTodoEntry { } /// Parses a GcTodoEntry from a (k, v) pair stored in the gc_todo tree - pub(crate) fn parse(sled_k: &[u8], sled_v: &[u8]) -> Self { + pub(crate) fn parse(db_k: &[u8], db_v: &[u8]) -> Self { Self { - tombstone_timestamp: u64::from_be_bytes(sled_k[0..8].try_into().unwrap()), - key: sled_k[8..].to_vec(), - value_hash: Hash::try_from(sled_v).unwrap(), + tombstone_timestamp: u64::from_be_bytes(db_k[0..8].try_into().unwrap()), + key: db_k[8..].to_vec(), + value_hash: Hash::try_from(db_v).unwrap(), value: None, } } /// Saves the GcTodoEntry in the gc_todo tree - pub(crate) fn save(&self, gc_todo_tree: &SledCountedTree) -> Result<(), Error> { + pub(crate) fn save(&self, gc_todo_tree: &CountedTree) -> Result<(), Error> { gc_todo_tree.insert(self.todo_table_key(), self.value_hash.as_slice())?; Ok(()) } @@ -373,9 +380,9 @@ impl GcTodoEntry { /// This is usefull to remove a todo entry only under the condition /// that it has not changed since the time it was read, i.e. /// what we have to do is still the same - pub(crate) fn remove_if_equal(&self, gc_todo_tree: &SledCountedTree) -> Result<(), Error> { - let _ = gc_todo_tree.compare_and_swap::<_, _, Vec>( - &self.todo_table_key()[..], + pub(crate) fn remove_if_equal(&self, gc_todo_tree: &CountedTree) -> Result<(), Error> { + gc_todo_tree.compare_and_swap::<_, _, &[u8]>( + &self.todo_table_key(), Some(self.value_hash), None, )?; diff --git a/src/table/merkle.rs b/src/table/merkle.rs index 93bf7e47..7685b193 100644 --- a/src/table/merkle.rs +++ b/src/table/merkle.rs @@ -4,11 +4,10 @@ use std::time::Duration; use futures::select; use futures_util::future::*; use serde::{Deserialize, Serialize}; -use sled::transaction::{ - ConflictableTransactionError, ConflictableTransactionResult, TransactionalTree, -}; use tokio::sync::watch; +use garage_db as db; + use garage_util::background::BackgroundRunner; use garage_util::data::*; use garage_util::error::Error; @@ -90,35 +89,35 @@ where async fn updater_loop(self: Arc, mut must_exit: watch::Receiver) { while !*must_exit.borrow() { - if let Some(x) = self.data.merkle_todo.iter().next() { - match x { - Ok((key, valhash)) => { - if let Err(e) = self.update_item(&key[..], &valhash[..]) { - warn!( - "({}) Error while updating Merkle tree item: {}", - F::TABLE_NAME, - e - ); - } - } - Err(e) => { - warn!( - "({}) Error while iterating on Merkle todo tree: {}", - F::TABLE_NAME, - e - ); - tokio::time::sleep(Duration::from_secs(10)).await; + match self.updater_loop_iter() { + Ok(true) => (), + Ok(false) => { + select! { + _ = self.data.merkle_todo_notify.notified().fuse() => {}, + _ = must_exit.changed().fuse() => {}, } } - } else { - select! { - _ = self.data.merkle_todo_notify.notified().fuse() => {}, - _ = must_exit.changed().fuse() => {}, + Err(e) => { + warn!( + "({}) Error while updating Merkle tree item: {}", + F::TABLE_NAME, + e + ); + tokio::time::sleep(Duration::from_secs(10)).await; } } } } + fn updater_loop_iter(&self) -> Result { + if let Some((key, valhash)) = self.data.merkle_todo.first()? { + self.update_item(&key, &valhash)?; + Ok(true) + } else { + Ok(false) + } + } + fn update_item(&self, k: &[u8], vhash_by: &[u8]) -> Result<(), Error> { let khash = blake2sum(k); @@ -137,13 +136,16 @@ where }; self.data .merkle_tree - .transaction(|tx| self.update_item_rec(tx, k, &khash, &key, new_vhash))?; + .db() + .transaction(|mut tx| self.update_item_rec(&mut tx, k, &khash, &key, new_vhash))?; - let deleted = self - .data - .merkle_todo - .compare_and_swap::<_, _, Vec>(k, Some(vhash_by), None)? - .is_ok(); + let deleted = self.data.merkle_todo.db().transaction(|mut tx| { + let remove = matches!(tx.get(&self.data.merkle_todo, k)?, Some(ov) if ov == vhash_by); + if remove { + tx.remove(&self.data.merkle_todo, k)?; + } + Ok(remove) + })?; if !deleted { debug!( @@ -157,12 +159,12 @@ where fn update_item_rec( &self, - tx: &TransactionalTree, + tx: &mut db::Transaction<'_>, k: &[u8], khash: &Hash, key: &MerkleNodeKey, new_vhash: Option, - ) -> ConflictableTransactionResult, Error> { + ) -> db::TxResult, Error> { let i = key.prefix.len(); // Read node at current position (defined by the prefix stored in key) @@ -203,7 +205,7 @@ where } MerkleNode::Intermediate(_) => Some(MerkleNode::Intermediate(children)), x @ MerkleNode::Leaf(_, _) => { - tx.remove(key_sub.encode())?; + tx.remove(&self.data.merkle_tree, key_sub.encode())?; Some(x) } } @@ -283,28 +285,27 @@ where fn read_node_txn( &self, - tx: &TransactionalTree, + tx: &mut db::Transaction<'_>, k: &MerkleNodeKey, - ) -> ConflictableTransactionResult { - let ent = tx.get(k.encode())?; - MerkleNode::decode_opt(ent).map_err(ConflictableTransactionError::Abort) + ) -> db::TxResult { + let ent = tx.get(&self.data.merkle_tree, k.encode())?; + MerkleNode::decode_opt(&ent).map_err(db::TxError::Abort) } fn put_node_txn( &self, - tx: &TransactionalTree, + tx: &mut db::Transaction<'_>, k: &MerkleNodeKey, v: &MerkleNode, - ) -> ConflictableTransactionResult { + ) -> db::TxResult { trace!("Put Merkle node: {:?} => {:?}", k, v); if *v == MerkleNode::Empty { - tx.remove(k.encode())?; + tx.remove(&self.data.merkle_tree, k.encode())?; Ok(self.empty_node_hash) } else { - let vby = rmp_to_vec_all_named(v) - .map_err(|e| ConflictableTransactionError::Abort(e.into()))?; + let vby = rmp_to_vec_all_named(v).map_err(|e| db::TxError::Abort(e.into()))?; let rethash = blake2sum(&vby[..]); - tx.insert(k.encode(), vby)?; + tx.insert(&self.data.merkle_tree, k.encode(), vby)?; Ok(rethash) } } @@ -312,15 +313,15 @@ where // Access a node in the Merkle tree, used by the sync protocol pub(crate) fn read_node(&self, k: &MerkleNodeKey) -> Result { let ent = self.data.merkle_tree.get(k.encode())?; - MerkleNode::decode_opt(ent) + MerkleNode::decode_opt(&ent) } - pub fn merkle_tree_len(&self) -> usize { - self.data.merkle_tree.len() + pub fn merkle_tree_len(&self) -> Result { + Ok(self.data.merkle_tree.len()?) } - pub fn todo_len(&self) -> usize { - self.data.merkle_todo.len() + pub fn todo_len(&self) -> Result { + Ok(self.data.merkle_todo.len()?) } } @@ -347,7 +348,7 @@ impl MerkleNodeKey { } impl MerkleNode { - fn decode_opt(ent: Option) -> Result { + fn decode_opt(ent: &Option) -> Result { match ent { None => Ok(MerkleNode::Empty), Some(v) => Ok(rmp_serde::decode::from_read_ref::<_, MerkleNode>(&v[..])?), diff --git a/src/table/metrics.rs b/src/table/metrics.rs index 752a2a6d..3a1783e0 100644 --- a/src/table/metrics.rs +++ b/src/table/metrics.rs @@ -1,6 +1,7 @@ use opentelemetry::{global, metrics::*, KeyValue}; -use garage_util::sled_counter::SledCountedTree; +use garage_db as db; +use garage_db::counted_tree_hack::CountedTree; /// TableMetrics reference all counter used for metrics pub struct TableMetrics { @@ -19,21 +20,19 @@ pub struct TableMetrics { pub(crate) sync_items_received: Counter, } impl TableMetrics { - pub fn new( - table_name: &'static str, - merkle_todo: sled::Tree, - gc_todo: SledCountedTree, - ) -> Self { + pub fn new(table_name: &'static str, merkle_todo: db::Tree, gc_todo: CountedTree) -> Self { let meter = global::meter(table_name); TableMetrics { _merkle_todo_len: meter .u64_value_observer( "table.merkle_updater_todo_queue_length", move |observer| { - observer.observe( - merkle_todo.len() as u64, - &[KeyValue::new("table_name", table_name)], - ) + if let Ok(v) = merkle_todo.len() { + observer.observe( + v as u64, + &[KeyValue::new("table_name", table_name)], + ); + } }, ) .with_description("Merkle tree updater TODO queue length") @@ -45,7 +44,7 @@ impl TableMetrics { observer.observe( gc_todo.len() as u64, &[KeyValue::new("table_name", table_name)], - ) + ); }, ) .with_description("Table garbage collector TODO queue length") diff --git a/src/table/schema.rs b/src/table/schema.rs index 37327037..74f57798 100644 --- a/src/table/schema.rs +++ b/src/table/schema.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; +use garage_db as db; use garage_util::data::*; use crate::crdt::Crdt; @@ -82,11 +83,19 @@ pub trait TableSchema: Send + Sync { None } - // Updated triggers some stuff downstream, but it is not supposed to block or fail, - // as the update itself is an unchangeable fact that will never go back - // due to CRDT logic. Typically errors in propagation of info should be logged - // to stderr. - fn updated(&self, _old: Option<&Self::E>, _new: Option<&Self::E>) {} + /// Actions triggered by data changing in a table. If such actions + /// include updates to the local database that should be applied + /// atomically with the item update itself, a db transaction is + /// provided on which these changes should be done. + /// This function can return a DB error but that's all. + fn updated( + &self, + _tx: &mut db::Transaction, + _old: Option<&Self::E>, + _new: Option<&Self::E>, + ) -> db::TxOpResult<()> { + Ok(()) + } fn matches_filter(entry: &Self::E, filter: &Self::Filter) -> bool; } diff --git a/src/table/sync.rs b/src/table/sync.rs index 08069ad0..4c83e991 100644 --- a/src/table/sync.rs +++ b/src/table/sync.rs @@ -258,9 +258,9 @@ where while !*must_exit.borrow() { let mut items = Vec::new(); - for item in self.data.store.range(begin.to_vec()..end.to_vec()) { + for item in self.data.store.range(begin.to_vec()..end.to_vec())? { let (key, value) = item?; - items.push((key.to_vec(), Arc::new(ByteBuf::from(value.as_ref())))); + items.push((key.to_vec(), Arc::new(ByteBuf::from(value)))); if items.len() >= 1024 { break; @@ -603,8 +603,16 @@ impl SyncTodo { let retain = nodes.contains(&my_id); if !retain { // Check if we have some data to send, otherwise skip - if data.store.range(begin..end).next().is_none() { - continue; + match data.store.range(begin..end) { + Ok(mut iter) => { + if iter.next().is_none() { + continue; + } + } + Err(e) => { + warn!("DB error in add_full_sync: {}", e); + continue; + } } } diff --git a/src/table/table.rs b/src/table/table.rs index 2a167604..3c211728 100644 --- a/src/table/table.rs +++ b/src/table/table.rs @@ -13,6 +13,8 @@ use opentelemetry::{ Context, }; +use garage_db as db; + use garage_util::data::*; use garage_util::error::Error; use garage_util::metrics::RecordDuration; @@ -69,7 +71,7 @@ where { // =============== PUBLIC INTERFACE FUNCTIONS (new, insert, get, etc) =============== - pub fn new(instance: F, replication: R, system: Arc, db: &sled::Db) -> Arc { + pub fn new(instance: F, replication: R, system: Arc, db: &db::Db) -> Arc { let endpoint = system .netapp .endpoint(format!("garage_table/table.rs/Rpc:{}", F::TABLE_NAME)); diff --git a/src/util/Cargo.toml b/src/util/Cargo.toml index 95cde531..5d073436 100644 --- a/src/util/Cargo.toml +++ b/src/util/Cargo.toml @@ -14,6 +14,8 @@ path = "lib.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +garage_db = { version = "0.8.0", path = "../db" } + blake2 = "0.9" err-derive = "0.3" xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] } @@ -22,8 +24,6 @@ tracing = "0.1.30" rand = "0.8" sha2 = "0.9" -sled = "0.34" - chrono = "0.4" rmp-serde = "0.15" serde = { version = "1.0", default-features = false, features = ["derive", "rc"] } diff --git a/src/util/config.rs b/src/util/config.rs index 99ebce31..e8ef4fdd 100644 --- a/src/util/config.rs +++ b/src/util/config.rs @@ -64,14 +64,19 @@ pub struct Config { #[serde(default)] pub kubernetes_skip_crd: bool, + // -- DB + /// Database engine to use for metadata (options: sled, sqlite, lmdb) + #[serde(default = "default_db_engine")] + pub db_engine: String, + /// Sled cache size, in bytes #[serde(default = "default_sled_cache_capacity")] pub sled_cache_capacity: u64, - /// Sled flush interval in milliseconds #[serde(default = "default_sled_flush_every_ms")] pub sled_flush_every_ms: u64, + // -- APIs /// Configuration for S3 api pub s3_api: S3ApiConfig, @@ -129,6 +134,10 @@ pub struct AdminConfig { pub trace_sink: Option, } +fn default_db_engine() -> String { + "sled".into() +} + fn default_sled_cache_capacity() -> u64 { 128 * 1024 * 1024 } diff --git a/src/util/error.rs b/src/util/error.rs index 8734a0c8..9995c746 100644 --- a/src/util/error.rs +++ b/src/util/error.rs @@ -26,8 +26,8 @@ pub enum Error { #[error(display = "Netapp error: {}", _0)] Netapp(#[error(source)] netapp::error::Error), - #[error(display = "Sled error: {}", _0)] - Sled(#[error(source)] sled::Error), + #[error(display = "DB error: {}", _0)] + Db(#[error(source)] garage_db::Error), #[error(display = "Messagepack encode error: {}", _0)] RmpEncode(#[error(source)] rmp_serde::encode::Error), @@ -78,11 +78,11 @@ impl Error { } } -impl From> for Error { - fn from(e: sled::transaction::TransactionError) -> Error { +impl From> for Error { + fn from(e: garage_db::TxError) -> Error { match e { - sled::transaction::TransactionError::Abort(x) => x, - sled::transaction::TransactionError::Storage(x) => Error::Sled(x), + garage_db::TxError::Abort(x) => x, + garage_db::TxError::Db(x) => Error::Db(x), } } } diff --git a/src/util/lib.rs b/src/util/lib.rs index d8ffdd0b..8ca6e310 100644 --- a/src/util/lib.rs +++ b/src/util/lib.rs @@ -11,7 +11,7 @@ pub mod error; pub mod formater; pub mod metrics; pub mod persister; -pub mod sled_counter; +//pub mod sled_counter; pub mod time; pub mod token_bucket; pub mod tranquilizer; diff --git a/src/util/sled_counter.rs b/src/util/sled_counter.rs deleted file mode 100644 index bc54cea0..00000000 --- a/src/util/sled_counter.rs +++ /dev/null @@ -1,100 +0,0 @@ -use std::sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, -}; - -use sled::{CompareAndSwapError, IVec, Iter, Result, Tree}; - -#[derive(Clone)] -pub struct SledCountedTree(Arc); - -struct SledCountedTreeInternal { - tree: Tree, - len: AtomicUsize, -} - -impl SledCountedTree { - pub fn new(tree: Tree) -> Self { - let len = tree.len(); - Self(Arc::new(SledCountedTreeInternal { - tree, - len: AtomicUsize::new(len), - })) - } - - pub fn len(&self) -> usize { - self.0.len.load(Ordering::Relaxed) - } - - pub fn is_empty(&self) -> bool { - self.0.tree.is_empty() - } - - pub fn get>(&self, key: K) -> Result> { - self.0.tree.get(key) - } - - pub fn iter(&self) -> Iter { - self.0.tree.iter() - } - - // ---- writing functions ---- - - pub fn insert(&self, key: K, value: V) -> Result> - where - K: AsRef<[u8]>, - V: Into, - { - let res = self.0.tree.insert(key, value); - if res == Ok(None) { - self.0.len.fetch_add(1, Ordering::Relaxed); - } - res - } - - pub fn remove>(&self, key: K) -> Result> { - let res = self.0.tree.remove(key); - if matches!(res, Ok(Some(_))) { - self.0.len.fetch_sub(1, Ordering::Relaxed); - } - res - } - - pub fn pop_min(&self) -> Result> { - let res = self.0.tree.pop_min(); - if let Ok(Some(_)) = &res { - self.0.len.fetch_sub(1, Ordering::Relaxed); - }; - res - } - - pub fn compare_and_swap( - &self, - key: K, - old: Option, - new: Option, - ) -> Result> - where - K: AsRef<[u8]>, - OV: AsRef<[u8]>, - NV: Into, - { - let old_some = old.is_some(); - let new_some = new.is_some(); - - let res = self.0.tree.compare_and_swap(key, old, new); - - if res == Ok(Ok(())) { - match (old_some, new_some) { - (false, true) => { - self.0.len.fetch_add(1, Ordering::Relaxed); - } - (true, false) => { - self.0.len.fetch_sub(1, Ordering::Relaxed); - } - _ => (), - } - } - res - } -} From 138e13071be37d873344cd03e316c87ff8057ea0 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Thu, 9 Jun 2022 14:55:20 +0200 Subject: [PATCH 022/149] Fix garage_db build on 32-bit systems --- src/db/bin/convert.rs | 9 +-------- src/db/lmdb_adapter.rs | 14 ++++++++++++++ src/garage/server.rs | 7 +------ 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/db/bin/convert.rs b/src/db/bin/convert.rs index 9e45e61f..bbde2048 100644 --- a/src/db/bin/convert.rs +++ b/src/db/bin/convert.rs @@ -55,14 +55,7 @@ fn open_db(path: PathBuf, engine: String) -> Result { Error(format!("Unable to create LMDB data directory: {}", e).into()) })?; - let map_size = if u32::MAX as usize == usize::MAX { - eprintln!( - "LMDB is not recommended on 32-bit systems, database size will be limited" - ); - 1usize << 30 // 1GB for 32-bit systems - } else { - 1usize << 40 // 1TB for 64-bit systems - }; + let map_size = lmdb_adapter::recommended_map_size(); let db = lmdb_adapter::heed::EnvOpenOptions::new() .max_dbs(100) diff --git a/src/db/lmdb_adapter.rs b/src/db/lmdb_adapter.rs index 74622919..62fcc3e6 100644 --- a/src/db/lmdb_adapter.rs +++ b/src/db/lmdb_adapter.rs @@ -327,3 +327,17 @@ where } } } + +// ---- + +#[cfg(target_pointer_width = "64")] +pub fn recommended_map_size() -> usize { + 1usize << 40 +} + +#[cfg(target_pointer_width = "32")] +pub fn recommended_map_size() -> usize { + use log::warn; + warn!("LMDB is not recommended on 32-bit systems, database size will be limited"); + 1usize << 30 +} diff --git a/src/garage/server.rs b/src/garage/server.rs index 697d3358..7aa6185f 100644 --- a/src/garage/server.rs +++ b/src/garage/server.rs @@ -57,12 +57,7 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> { db_path.push("db.lmdb"); info!("Opening LMDB database at: {}", db_path.display()); std::fs::create_dir_all(&db_path).expect("Unable to create LMDB data directory"); - let map_size = if u32::MAX as usize == usize::MAX { - warn!("LMDB is not recommended on 32-bit systems, database size will be limited"); - 1usize << 30 // 1GB for 32-bit systems - } else { - 1usize << 40 // 1TB for 64-bit systems - }; + let map_size = garage_db::lmdb_adapter::recommended_map_size(); let db = db::lmdb_adapter::heed::EnvOpenOptions::new() .max_dbs(100) From d544a0e0e03c9b69b226fb5bba2ce27a7af270ca Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Mon, 13 Jun 2022 10:13:31 +0200 Subject: [PATCH 023/149] Send CORS headers for all requests --- src/api/s3/api_server.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/api/s3/api_server.rs b/src/api/s3/api_server.rs index ecc417ab..d1d6288c 100644 --- a/src/api/s3/api_server.rs +++ b/src/api/s3/api_server.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use futures::future::Future; use hyper::header; -use hyper::{Body, Method, Request, Response}; +use hyper::{Body, Request, Response}; use opentelemetry::{trace::SpanRef, KeyValue}; @@ -167,14 +167,7 @@ impl ApiHandler for S3ApiServer { return Err(Error::forbidden("Operation is not allowed for this key.")); } - // Look up what CORS rule might apply to response. - // Requests for methods different than GET, HEAD or POST - // are always preflighted, i.e. the browser should make - // an OPTIONS call before to check it is allowed - let matching_cors_rule = match *req.method() { - Method::GET | Method::HEAD | Method::POST => find_matching_cors_rule(&bucket, &req)?, - _ => None, - }; + let matching_cors_rule = find_matching_cors_rule(&bucket, &req)?; let resp = match endpoint { Endpoint::HeadObject { From 77e3fd6db2c9cd3a10889bd071e95ef839cfbefc Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 15 Jun 2022 20:20:28 +0200 Subject: [PATCH 024/149] improve internal item counter mechanisms and implement bucket quotas (#326) - [x] Refactoring of internal counting API - [x] Repair procedure for counters (it's an offline procedure!!!) - [x] New counter for objects in buckets - [x] Add quotas to buckets struct - [x] Add CLI to manage bucket quotas - [x] Add admin API to manage bucket quotas - [x] Apply quotas by adding checks on put operations - [x] Proof-read Co-authored-by: Alex Auvolat Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/326 Co-authored-by: Alex Co-committed-by: Alex --- Cargo.lock | 7 + Cargo.nix | 18 +- doc/book/reference-manual/admin-api.md | 158 +++++++------ src/api/admin/api_server.rs | 7 +- src/api/admin/bucket.rs | 96 +++++--- src/api/admin/router.rs | 8 +- src/api/k2v/index.rs | 2 +- src/api/s3/api_server.rs | 4 +- src/api/s3/post_object.rs | 15 +- src/api/s3/put.rs | 129 ++++++++--- src/db/lib.rs | 6 + src/db/lmdb_adapter.rs | 8 + src/db/sled_adapter.rs | 6 + src/db/sqlite_adapter.rs | 10 + src/garage/Cargo.toml | 1 + src/garage/admin.rs | 82 ++++++- src/garage/cli/cmd.rs | 8 +- src/garage/cli/structs.rs | 47 +++- src/garage/cli/util.rs | 47 +++- src/garage/main.rs | 18 +- src/garage/repair/mod.rs | 2 + src/garage/repair/offline.rs | 55 +++++ src/garage/{repair.rs => repair/online.rs} | 4 +- src/garage/server.rs | 50 +---- src/model/bucket_table.rs | 19 +- src/model/garage.rs | 67 +++++- src/model/index_counter.rs | 250 +++++++++++++++++---- src/model/k2v/counter_table.rs | 20 -- src/model/k2v/item_table.rs | 102 +++++---- src/model/k2v/mod.rs | 1 - src/model/migrate.rs | 1 + src/model/s3/object_table.rs | 61 ++++- 32 files changed, 962 insertions(+), 347 deletions(-) create mode 100644 src/garage/repair/mod.rs create mode 100644 src/garage/repair/offline.rs rename src/garage/{repair.rs => repair/online.rs} (98%) delete mode 100644 src/model/k2v/counter_table.rs diff --git a/Cargo.lock b/Cargo.lock index 11aa070d..ecdf8a57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -387,6 +387,12 @@ dependencies = [ "either", ] +[[package]] +name = "bytesize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70" + [[package]] name = "cc" version = "1.0.73" @@ -948,6 +954,7 @@ dependencies = [ "aws-sdk-s3", "base64", "bytes 1.1.0", + "bytesize", "chrono", "futures", "futures-util", diff --git a/Cargo.nix b/Cargo.nix index 335651fc..e5155e61 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -564,6 +564,13 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".bytesize."1.1.0" = overridableMkRustCrate (profileName: rec { + name = "bytesize"; + version = "1.1.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70"; }; + }); + "registry+https://github.com/rust-lang/crates.io-index".cc."1.0.73" = overridableMkRustCrate (profileName: rec { name = "cc"; version = "1.0.73"; @@ -732,7 +739,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.config == "aarch64-linux-android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -1353,6 +1360,7 @@ in dependencies = { async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytesize = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytesize."1.1.0" { inherit profileName; }; futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; garage_api = rustPackages."unknown".garage_api."0.7.0" { inherit profileName; }; @@ -2718,7 +2726,7 @@ in [ "os-poll" ] ]; dependencies = { - ${ if hostPlatform.parsed.kernel.name == "wasi" || hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "wasi" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if hostPlatform.isWindows then "miow" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".miow."0.3.7" { inherit profileName; }; ${ if hostPlatform.isWindows then "ntapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ntapi."0.3.7" { inherit profileName; }; @@ -3730,7 +3738,7 @@ in ]; dependencies = { ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" || hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "i686" || hostPlatform.parsed.cpu.name == "x86_64" || (hostPlatform.parsed.cpu.name == "aarch64" || hostPlatform.parsed.cpu.name == "armv6l" || hostPlatform.parsed.cpu.name == "armv7l") && (hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "fuchsia" || hostPlatform.parsed.kernel.name == "linux") then "spin" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".spin."0.5.2" { inherit profileName; }; untrusted = rustPackages."registry+https://github.com/rust-lang/crates.io-index".untrusted."0.7.1" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "wasm32" && hostPlatform.parsed.vendor.name == "unknown" && hostPlatform.parsed.kernel.name == "unknown" && hostPlatform.parsed.abi.name == "" then "web_sys" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.56" { inherit profileName; }; @@ -4319,7 +4327,7 @@ in ]; dependencies = { bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot_core."0.8.5" { inherit profileName; }; static_init_macro = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init_macro."1.0.2" { profileName = "__noProfile"; }; @@ -5325,7 +5333,7 @@ in ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); diff --git a/doc/book/reference-manual/admin-api.md b/doc/book/reference-manual/admin-api.md index b77f0d39..c7316cdf 100644 --- a/doc/book/reference-manual/admin-api.md +++ b/doc/book/reference-manual/admin-api.md @@ -134,8 +134,8 @@ Example request body: ```json [ - "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f@10.0.0.11:3901", - "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff@10.0.0.12:3901" + "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f@10.0.0.11:3901", + "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff@10.0.0.12:3901" ] ``` @@ -145,14 +145,14 @@ Example response: ```json [ - { - "success": true, - "error": null - }, - { - "success": false, - "error": "Handshake error" - } + { + "success": true, + "error": null + }, + { + "success": false, + "error": "Handshake error" + } ] ``` @@ -301,7 +301,7 @@ Request body format: ```json { - "name": "NameOfMyKey" + "name": "NameOfMyKey" } ``` @@ -313,9 +313,9 @@ Request body format: ```json { - "accessKeyId": "GK31c2f218a2e44f485b94239e", - "secretAccessKey": "b892c0665f0ada8a4755dae98baa3b133590e11dae3bcc1f9d769d67f16c3835", - "name": "NameOfMyKey" + "accessKeyId": "GK31c2f218a2e44f485b94239e", + "secretAccessKey": "b892c0665f0ada8a4755dae98baa3b133590e11dae3bcc1f9d769d67f16c3835", + "name": "NameOfMyKey" } ``` @@ -403,11 +403,11 @@ Request body format: ```json { - "name": "NameOfMyKey", - "allow": { - "createBucket": true, - }, - "deny": {} + "name": "NameOfMyKey", + "allow": { + "createBucket": true, + }, + "deny": {} } ``` @@ -473,24 +473,31 @@ Example response: ```json { - "id": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b", - "globalAliases": [ - "alex" - ], - "keys": [ - { - "accessKeyId": "GK31c2f218a2e44f485b94239e", - "name": "alex", - "permissions": { - "read": true, - "write": true, - "owner": true - }, - "bucketLocalAliases": [ - "test" - ] - } - ] + "id": "afa8f0a22b40b1247ccd0affb869b0af5cff980924a20e4b5e0720a44deb8d39", + "globalAliases": [], + "websiteAccess": false, + "websiteConfig": null, + "keys": [ + { + "accessKeyId": "GK31c2f218a2e44f485b94239e", + "name": "Imported key", + "permissions": { + "read": true, + "write": true, + "owner": true + }, + "bucketLocalAliases": [ + "debug" + ] + } + ], + "objects": 14827, + "bytes": 13189855625, + "unfinshedUploads": 0, + "quotas": { + "maxSize": null, + "maxObjects": null + } } ``` @@ -502,7 +509,7 @@ Request body format: ```json { - "globalAlias": "NameOfMyBucket" + "globalAlias": "NameOfMyBucket" } ``` @@ -510,15 +517,15 @@ OR ```json { - "localAlias": { - "accessKeyId": "GK31c2f218a2e44f485b94239e", - "alias": "NameOfMyBucket", - "allow": { - "read": true, - "write": true, - "owner": false - } - } + "localAlias": { + "accessKeyId": "GK31c2f218a2e44f485b94239e", + "alias": "NameOfMyBucket", + "allow": { + "read": true, + "write": true, + "owner": false + } + } } ``` @@ -540,26 +547,37 @@ Deletes a storage bucket. A bucket cannot be deleted if it is not empty. Warning: this will delete all aliases associated with the bucket! -#### PutBucketWebsite `PUT /v0/bucket/website?id=` +#### UpdateBucket `PUT /v0/bucket?id=` -Sets the website configuration for a bucket (this also enables website access for this bucket). +Updates configuration of the given bucket. Request body format: ```json { - "indexDocument": "index.html", - "errorDocument": "404.html" + "websiteAccess": { + "enabled": true, + "indexDocument": "index.html", + "errorDocument": "404.html" + }, + "quotas": { + "maxSize": 19029801, + "maxObjects": null, + } } ``` -The field `errorDocument` is optional, if no error document is set a generic error message is displayed when errors happen. +All fields (`websiteAccess` and `quotas`) are optionnal. +If they are present, the corresponding modifications are applied to the bucket, otherwise nothing is changed. +In `websiteAccess`: if `enabled` is `true`, `indexDocument` must be specified. +The field `errorDocument` is optional, if no error document is set a generic +error message is displayed when errors happen. Conversely, if `enabled` is +`false`, neither `indexDocument` nor `errorDocument` must be specified. -#### DeleteBucketWebsite `DELETE /v0/bucket/website?id=` - -Deletes the website configuration for a bucket (disables website access for this bucket). - +In `quotas`: new values of `maxSize` and `maxObjects` must both be specified, or set to `null` +to remove the quotas. An absent value will be considered the same as a `null`. It is not possible +to change only one of the two quotas. ### Operations on permissions for keys on buckets @@ -571,13 +589,13 @@ Request body format: ```json { - "bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b", - "accessKeyId": "GK31c2f218a2e44f485b94239e", - "permissions": { - "read": true, - "write": true, - "owner": true - }, + "bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b", + "accessKeyId": "GK31c2f218a2e44f485b94239e", + "permissions": { + "read": true, + "write": true, + "owner": true + }, } ``` @@ -592,13 +610,13 @@ Request body format: ```json { - "bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b", - "accessKeyId": "GK31c2f218a2e44f485b94239e", - "permissions": { - "read": false, - "write": false, - "owner": true - }, + "bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b", + "accessKeyId": "GK31c2f218a2e44f485b94239e", + "permissions": { + "read": false, + "write": false, + "owner": true + }, } ``` diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs index 57e3e5cf..c3b16715 100644 --- a/src/api/admin/api_server.rs +++ b/src/api/admin/api_server.rs @@ -156,12 +156,7 @@ impl ApiHandler for AdminApiServer { } Endpoint::CreateBucket => handle_create_bucket(&self.garage, req).await, Endpoint::DeleteBucket { id } => handle_delete_bucket(&self.garage, id).await, - Endpoint::PutBucketWebsite { id } => { - handle_put_bucket_website(&self.garage, id, req).await - } - Endpoint::DeleteBucketWebsite { id } => { - handle_delete_bucket_website(&self.garage, id).await - } + Endpoint::UpdateBucket { id } => handle_update_bucket(&self.garage, id, req).await, // Bucket-key permissions Endpoint::BucketAllowKey => { handle_bucket_change_key_perm(&self.garage, req, true).await diff --git a/src/api/admin/bucket.rs b/src/api/admin/bucket.rs index 7f9a813f..ac8a8a40 100644 --- a/src/api/admin/bucket.rs +++ b/src/api/admin/bucket.rs @@ -14,6 +14,7 @@ use garage_model::bucket_alias_table::*; use garage_model::bucket_table::*; use garage_model::garage::Garage; use garage_model::permission::*; +use garage_model::s3::object_table::*; use crate::admin::error::*; use crate::admin::key::ApiBucketKeyPerm; @@ -77,6 +78,13 @@ struct BucketLocalAlias { alias: String, } +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ApiBucketQuotas { + max_size: Option, + max_objects: Option, +} + pub async fn handle_get_bucket_info( garage: &Arc, id: Option, @@ -108,6 +116,14 @@ async fn bucket_info_results( .get_existing_bucket(bucket_id) .await?; + let counters = garage + .object_counter_table + .table + .get(&bucket_id, &EmptyKey) + .await? + .map(|x| x.filtered_values(&garage.system.ring.borrow())) + .unwrap_or_default(); + let mut relevant_keys = HashMap::new(); for (k, _) in bucket .state @@ -148,6 +164,7 @@ async fn bucket_info_results( let state = bucket.state.as_option().unwrap(); + let quotas = state.quotas.get(); let res = GetBucketInfoResult { id: hex::encode(&bucket.id), @@ -191,6 +208,16 @@ async fn bucket_info_results( } }) .collect::>(), + objects: counters.get(OBJECTS).cloned().unwrap_or_default(), + bytes: counters.get(BYTES).cloned().unwrap_or_default(), + unfinshed_uploads: counters + .get(UNFINISHED_UPLOADS) + .cloned() + .unwrap_or_default(), + quotas: ApiBucketQuotas { + max_size: quotas.max_size, + max_objects: quotas.max_objects, + }, }; Ok(json_ok_response(&res)?) @@ -205,6 +232,10 @@ struct GetBucketInfoResult { #[serde(default)] website_config: Option, keys: Vec, + objects: i64, + bytes: i64, + unfinshed_uploads: i64, + quotas: ApiBucketQuotas, } #[derive(Serialize)] @@ -363,14 +394,12 @@ pub async fn handle_delete_bucket( .body(Body::empty())?) } -// ---- BUCKET WEBSITE CONFIGURATION ---- - -pub async fn handle_put_bucket_website( +pub async fn handle_update_bucket( garage: &Arc, id: String, req: Request, ) -> Result, Error> { - let req = parse_json_body::(req).await?; + let req = parse_json_body::(req).await?; let bucket_id = parse_bucket_id(&id)?; let mut bucket = garage @@ -379,10 +408,31 @@ pub async fn handle_put_bucket_website( .await?; let state = bucket.state.as_option_mut().unwrap(); - state.website_config.update(Some(WebsiteConfig { - index_document: req.index_document, - error_document: req.error_document, - })); + + if let Some(wa) = req.website_access { + if wa.enabled { + state.website_config.update(Some(WebsiteConfig { + index_document: wa.index_document.ok_or_bad_request( + "Please specify indexDocument when enabling website access.", + )?, + error_document: wa.error_document, + })); + } else { + if wa.index_document.is_some() || wa.error_document.is_some() { + return Err(Error::bad_request( + "Cannot specify indexDocument or errorDocument when disabling website access.", + )); + } + state.website_config.update(None); + } + } + + if let Some(q) = req.quotas { + state.quotas.update(BucketQuotas { + max_size: q.max_size, + max_objects: q.max_objects, + }); + } garage.bucket_table.insert(&bucket).await?; @@ -391,29 +441,17 @@ pub async fn handle_put_bucket_website( #[derive(Deserialize)] #[serde(rename_all = "camelCase")] -struct PutBucketWebsiteRequest { - index_document: String, - #[serde(default)] - error_document: Option, +struct UpdateBucketRequest { + website_access: Option, + quotas: Option, } -pub async fn handle_delete_bucket_website( - garage: &Arc, - id: String, -) -> Result, Error> { - let bucket_id = parse_bucket_id(&id)?; - - let mut bucket = garage - .bucket_helper() - .get_existing_bucket(bucket_id) - .await?; - - let state = bucket.state.as_option_mut().unwrap(); - state.website_config.update(None); - - garage.bucket_table.insert(&bucket).await?; - - bucket_info_results(garage, bucket_id).await +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct UpdateBucketWebsiteAccess { + enabled: bool, + index_document: Option, + error_document: Option, } // ---- BUCKET/KEY PERMISSIONS ---- diff --git a/src/api/admin/router.rs b/src/api/admin/router.rs index 93639873..3eee8b67 100644 --- a/src/api/admin/router.rs +++ b/src/api/admin/router.rs @@ -48,10 +48,7 @@ pub enum Endpoint { DeleteBucket { id: String, }, - PutBucketWebsite { - id: String, - }, - DeleteBucketWebsite { + UpdateBucket { id: String, }, // Bucket-Key Permissions @@ -113,8 +110,7 @@ impl Endpoint { GET "/v0/bucket" => ListBuckets, POST "/v0/bucket" => CreateBucket, DELETE "/v0/bucket" if id => DeleteBucket (query::id), - PUT "/v0/bucket/website" if id => PutBucketWebsite (query::id), - DELETE "/v0/bucket/website" if id => DeleteBucketWebsite (query::id), + PUT "/v0/bucket" if id => UpdateBucket (query::id), // Bucket-key permissions POST "/v0/bucket/allow" => BucketAllowKey, POST "/v0/bucket/deny" => BucketDenyKey, diff --git a/src/api/k2v/index.rs b/src/api/k2v/index.rs index d5db906d..210950bf 100644 --- a/src/api/k2v/index.rs +++ b/src/api/k2v/index.rs @@ -10,7 +10,7 @@ use garage_rpc::ring::Ring; use garage_table::util::*; use garage_model::garage::Garage; -use garage_model::k2v::counter_table::{BYTES, CONFLICTS, ENTRIES, VALUES}; +use garage_model::k2v::item_table::{BYTES, CONFLICTS, ENTRIES, VALUES}; use crate::k2v::error::*; use crate::k2v::range::read_range; diff --git a/src/api/s3/api_server.rs b/src/api/s3/api_server.rs index d1d6288c..78dfeeac 100644 --- a/src/api/s3/api_server.rs +++ b/src/api/s3/api_server.rs @@ -212,7 +212,7 @@ impl ApiHandler for S3ApiServer { .await } Endpoint::PutObject { key } => { - handle_put(garage, req, bucket_id, &key, content_sha256).await + handle_put(garage, req, &bucket, &key, content_sha256).await } Endpoint::AbortMultipartUpload { key, upload_id } => { handle_abort_multipart_upload(garage, bucket_id, &key, &upload_id).await @@ -226,7 +226,7 @@ impl ApiHandler for S3ApiServer { garage, req, &bucket_name, - bucket_id, + &bucket, &key, &upload_id, content_sha256, diff --git a/src/api/s3/post_object.rs b/src/api/s3/post_object.rs index dc640f43..d063faa4 100644 --- a/src/api/s3/post_object.rs +++ b/src/api/s3/post_object.rs @@ -22,7 +22,7 @@ use crate::signature::payload::{parse_date, verify_v4}; pub async fn handle_post_object( garage: Arc, req: Request, - bucket: String, + bucket_name: String, ) -> Result, Error> { let boundary = req .headers() @@ -126,13 +126,18 @@ pub async fn handle_post_object( let bucket_id = garage .bucket_helper() - .resolve_bucket(&bucket, &api_key) + .resolve_bucket(&bucket_name, &api_key) .await?; if !api_key.allow_write(&bucket_id) { return Err(Error::forbidden("Operation is not allowed for this key.")); } + let bucket = garage + .bucket_helper() + .get_existing_bucket(bucket_id) + .await?; + let decoded_policy = base64::decode(&policy).ok_or_bad_request("Invalid policy")?; let decoded_policy: Policy = serde_json::from_slice(&decoded_policy).ok_or_bad_request("Invalid policy")?; @@ -227,7 +232,7 @@ pub async fn handle_post_object( garage, headers, StreamLimiter::new(stream, conditions.content_length), - bucket_id, + &bucket, &key, None, None, @@ -244,7 +249,7 @@ pub async fn handle_post_object( { target .query_pairs_mut() - .append_pair("bucket", &bucket) + .append_pair("bucket", &bucket_name) .append_pair("key", &key) .append_pair("etag", &etag); let target = target.to_string(); @@ -289,7 +294,7 @@ pub async fn handle_post_object( let xml = s3_xml::PostObject { xmlns: (), location: s3_xml::Value(location), - bucket: s3_xml::Value(bucket), + bucket: s3_xml::Value(bucket_name), key: s3_xml::Value(key), etag: s3_xml::Value(etag), }; diff --git a/src/api/s3/put.rs b/src/api/s3/put.rs index 8b06ef3f..9ef37421 100644 --- a/src/api/s3/put.rs +++ b/src/api/s3/put.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, BTreeSet, VecDeque}; +use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque}; use std::sync::Arc; use futures::prelude::*; @@ -14,7 +14,9 @@ use garage_util::error::Error as GarageError; use garage_util::time::*; use garage_block::manager::INLINE_THRESHOLD; +use garage_model::bucket_table::Bucket; use garage_model::garage::Garage; +use garage_model::index_counter::CountedItem; use garage_model::s3::block_ref_table::*; use garage_model::s3::object_table::*; use garage_model::s3::version_table::*; @@ -26,7 +28,7 @@ use crate::signature::verify_signed_content; pub async fn handle_put( garage: Arc, req: Request, - bucket_id: Uuid, + bucket: &Bucket, key: &str, content_sha256: Option, ) -> Result, Error> { @@ -46,7 +48,7 @@ pub async fn handle_put( garage, headers, body, - bucket_id, + bucket, key, content_md5, content_sha256, @@ -59,7 +61,7 @@ pub(crate) async fn save_stream> + Unpin>( garage: Arc, headers: ObjectVersionHeaders, body: S, - bucket_id: Uuid, + bucket: &Bucket, key: &str, content_md5: Option, content_sha256: Option, @@ -80,6 +82,7 @@ pub(crate) async fn save_stream> + Unpin>( let data_md5sum_hex = hex::encode(data_md5sum); let data_sha256sum = sha256sum(&first_block[..]); + let size = first_block.len() as u64; ensure_checksum_matches( data_md5sum.as_slice(), @@ -88,20 +91,22 @@ pub(crate) async fn save_stream> + Unpin>( content_sha256, )?; + check_quotas(&garage, bucket, key, size).await?; + let object_version = ObjectVersion { uuid: version_uuid, timestamp: version_timestamp, state: ObjectVersionState::Complete(ObjectVersionData::Inline( ObjectVersionMeta { headers, - size: first_block.len() as u64, + size, etag: data_md5sum_hex.clone(), }, first_block, )), }; - let object = Object::new(bucket_id, key.into(), vec![object_version]); + let object = Object::new(bucket.id, key.into(), vec![object_version]); garage.object_table.insert(&object).await?; return Ok((version_uuid, data_md5sum_hex)); @@ -114,36 +119,42 @@ pub(crate) async fn save_stream> + Unpin>( timestamp: version_timestamp, state: ObjectVersionState::Uploading(headers.clone()), }; - let object = Object::new(bucket_id, key.into(), vec![object_version.clone()]); + let object = Object::new(bucket.id, key.into(), vec![object_version.clone()]); garage.object_table.insert(&object).await?; // Initialize corresponding entry in version table // Write this entry now, even with empty block list, // to prevent block_ref entries from being deleted (they can be deleted // if the reference a version that isn't found in the version table) - let version = Version::new(version_uuid, bucket_id, key.into(), false); + let version = Version::new(version_uuid, bucket.id, key.into(), false); garage.version_table.insert(&version).await?; // Transfer data and verify checksum let first_block_hash = blake2sum(&first_block[..]); - let tx_result = read_and_put_blocks( - &garage, - &version, - 1, - first_block, - first_block_hash, - &mut chunker, - ) - .await - .and_then(|(total_size, data_md5sum, data_sha256sum)| { + + let tx_result = (|| async { + let (total_size, data_md5sum, data_sha256sum) = read_and_put_blocks( + &garage, + &version, + 1, + first_block, + first_block_hash, + &mut chunker, + ) + .await?; + ensure_checksum_matches( data_md5sum.as_slice(), data_sha256sum, content_md5.as_deref(), content_sha256, - ) - .map(|()| (total_size, data_md5sum)) - }); + )?; + + check_quotas(&garage, bucket, key, total_size).await?; + + Ok((total_size, data_md5sum)) + })() + .await; // If something went wrong, clean up let (total_size, md5sum_arr) = match tx_result { @@ -151,7 +162,7 @@ pub(crate) async fn save_stream> + Unpin>( Err(e) => { // Mark object as aborted, this will free the blocks further down object_version.state = ObjectVersionState::Aborted; - let object = Object::new(bucket_id, key.into(), vec![object_version.clone()]); + let object = Object::new(bucket.id, key.into(), vec![object_version.clone()]); garage.object_table.insert(&object).await?; return Err(e); } @@ -167,7 +178,7 @@ pub(crate) async fn save_stream> + Unpin>( }, first_block_hash, )); - let object = Object::new(bucket_id, key.into(), vec![object_version]); + let object = Object::new(bucket.id, key.into(), vec![object_version]); garage.object_table.insert(&object).await?; Ok((version_uuid, md5sum_hex)) @@ -200,6 +211,64 @@ fn ensure_checksum_matches( Ok(()) } +/// Check that inserting this object with this size doesn't exceed bucket quotas +async fn check_quotas( + garage: &Arc, + bucket: &Bucket, + key: &str, + size: u64, +) -> Result<(), Error> { + let quotas = bucket.state.as_option().unwrap().quotas.get(); + if quotas.max_objects.is_none() && quotas.max_size.is_none() { + return Ok(()); + }; + + let key = key.to_string(); + let (prev_object, counters) = futures::try_join!( + garage.object_table.get(&bucket.id, &key), + garage.object_counter_table.table.get(&bucket.id, &EmptyKey), + )?; + + let counters = counters + .map(|x| x.filtered_values(&garage.system.ring.borrow())) + .unwrap_or_default(); + + let (prev_cnt_obj, prev_cnt_size) = match prev_object { + Some(o) => { + let prev_cnt = o.counts().into_iter().collect::>(); + ( + prev_cnt.get(OBJECTS).cloned().unwrap_or_default(), + prev_cnt.get(BYTES).cloned().unwrap_or_default(), + ) + } + None => (0, 0), + }; + let cnt_obj_diff = 1 - prev_cnt_obj; + let cnt_size_diff = size as i64 - prev_cnt_size; + + if let Some(mo) = quotas.max_objects { + let current_objects = counters.get(OBJECTS).cloned().unwrap_or_default(); + if cnt_obj_diff > 0 && current_objects + cnt_obj_diff > mo as i64 { + return Err(Error::forbidden(format!( + "Object quota is reached, maximum objects for this bucket: {}", + mo + ))); + } + } + + if let Some(ms) = quotas.max_size { + let current_size = counters.get(BYTES).cloned().unwrap_or_default(); + if cnt_size_diff > 0 && current_size + cnt_size_diff > ms as i64 { + return Err(Error::forbidden(format!( + "Bucket size quota is reached, maximum total size of objects for this bucket: {}. The bucket is already {} bytes, and this object would add {} bytes.", + ms, current_size, size + ))); + } + } + + Ok(()) +} + async fn read_and_put_blocks> + Unpin>( garage: &Garage, version: &Version, @@ -473,7 +542,7 @@ pub async fn handle_complete_multipart_upload( garage: Arc, req: Request, bucket_name: &str, - bucket_id: Uuid, + bucket: &Bucket, key: &str, upload_id: &str, content_sha256: Option, @@ -497,7 +566,7 @@ pub async fn handle_complete_multipart_upload( // Get object and version let key = key.to_string(); let (object, version) = futures::try_join!( - garage.object_table.get(&bucket_id, &key), + garage.object_table.get(&bucket.id, &key), garage.version_table.get(&version_uuid, &EmptyKey), )?; @@ -590,6 +659,14 @@ pub async fn handle_complete_multipart_upload( // Calculate total size of final object let total_size = version.blocks.items().iter().map(|x| x.1.size).sum(); + if let Err(e) = check_quotas(&garage, bucket, &key, total_size).await { + object_version.state = ObjectVersionState::Aborted; + let final_object = Object::new(bucket.id, key.clone(), vec![object_version]); + garage.object_table.insert(&final_object).await?; + + return Err(e); + } + // Write final object version object_version.state = ObjectVersionState::Complete(ObjectVersionData::FirstBlock( ObjectVersionMeta { @@ -600,7 +677,7 @@ pub async fn handle_complete_multipart_upload( version.blocks.items()[0].1.hash, )); - let final_object = Object::new(bucket_id, key.clone(), vec![object_version]); + let final_object = Object::new(bucket.id, key.clone(), vec![object_version]); garage.object_table.insert(&final_object).await?; // Send response saying ok we're done diff --git a/src/db/lib.rs b/src/db/lib.rs index e9d3ea18..8188c715 100644 --- a/src/db/lib.rs +++ b/src/db/lib.rs @@ -197,6 +197,11 @@ impl Tree { pub fn remove>(&self, key: T) -> Result> { self.0.remove(self.1, key.as_ref()) } + /// Clears all values from the tree + #[inline] + pub fn clear(&self) -> Result<()> { + self.0.clear(self.1) + } #[inline] pub fn iter(&self) -> Result> { @@ -311,6 +316,7 @@ pub(crate) trait IDb: Send + Sync { fn insert(&self, tree: usize, key: &[u8], value: &[u8]) -> Result>; fn remove(&self, tree: usize, key: &[u8]) -> Result>; + fn clear(&self, tree: usize) -> Result<()>; fn iter(&self, tree: usize) -> Result>; fn iter_rev(&self, tree: usize) -> Result>; diff --git a/src/db/lmdb_adapter.rs b/src/db/lmdb_adapter.rs index 62fcc3e6..fdb254c6 100644 --- a/src/db/lmdb_adapter.rs +++ b/src/db/lmdb_adapter.rs @@ -139,6 +139,14 @@ impl IDb for LmdbDb { Ok(old_val) } + fn clear(&self, tree: usize) -> Result<()> { + let tree = self.get_tree(tree)?; + let mut tx = self.db.write_txn()?; + tree.clear(&mut tx)?; + tx.commit()?; + Ok(()) + } + fn iter(&self, tree: usize) -> Result> { let tree = self.get_tree(tree)?; let tx = self.db.read_txn()?; diff --git a/src/db/sled_adapter.rs b/src/db/sled_adapter.rs index 982f8d82..cf61867d 100644 --- a/src/db/sled_adapter.rs +++ b/src/db/sled_adapter.rs @@ -113,6 +113,12 @@ impl IDb for SledDb { Ok(old_val.map(|x| x.to_vec())) } + fn clear(&self, tree: usize) -> Result<()> { + let tree = self.get_tree(tree)?; + tree.clear()?; + Ok(()) + } + fn iter(&self, tree: usize) -> Result> { let tree = self.get_tree(tree)?; Ok(Box::new(tree.iter().map(|v| { diff --git a/src/db/sqlite_adapter.rs b/src/db/sqlite_adapter.rs index 14bf35ff..68d96ca0 100644 --- a/src/db/sqlite_adapter.rs +++ b/src/db/sqlite_adapter.rs @@ -182,6 +182,16 @@ impl IDb for SqliteDb { Ok(old_val) } + fn clear(&self, tree: usize) -> Result<()> { + trace!("clear {}: lock db", tree); + let this = self.0.lock().unwrap(); + trace!("clear {}: lock acquired", tree); + + let tree = this.get_tree(tree)?; + this.db.execute(&format!("DELETE FROM {}", tree), [])?; + Ok(()) + } + fn iter(&self, tree: usize) -> Result> { trace!("iter {}: lock db", tree); let this = self.0.lock().unwrap(); diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index eb643160..640e6975 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -30,6 +30,7 @@ garage_util = { version = "0.7.0", path = "../util" } garage_web = { version = "0.7.0", path = "../web" } bytes = "1.0" +bytesize = "1.1" hex = "0.4" tracing = { version = "0.1.30", features = ["log-always"] } pretty_env_logger = "0.4" diff --git a/src/garage/admin.rs b/src/garage/admin.rs index c662aa00..48914655 100644 --- a/src/garage/admin.rs +++ b/src/garage/admin.rs @@ -24,11 +24,12 @@ use garage_model::migrate::Migrate; use garage_model::permission::*; use crate::cli::*; -use crate::repair::Repair; +use crate::repair::online::OnlineRepair; pub const ADMIN_RPC_PATH: &str = "garage/admin_rpc.rs/Rpc"; #[derive(Debug, Serialize, Deserialize)] +#[allow(clippy::large_enum_variant)] pub enum AdminRpc { BucketOperation(BucketOperation), KeyOperation(KeyOperation), @@ -39,7 +40,11 @@ pub enum AdminRpc { // Replies Ok(String), BucketList(Vec), - BucketInfo(Bucket, HashMap), + BucketInfo { + bucket: Bucket, + relevant_keys: HashMap, + counters: HashMap, + }, KeyList(Vec<(String, String)>), KeyInfo(Key, HashMap), } @@ -72,6 +77,7 @@ impl AdminRpcHandler { BucketOperation::Allow(query) => self.handle_bucket_allow(query).await, BucketOperation::Deny(query) => self.handle_bucket_deny(query).await, BucketOperation::Website(query) => self.handle_bucket_website(query).await, + BucketOperation::SetQuotas(query) => self.handle_bucket_set_quotas(query).await, } } @@ -87,6 +93,7 @@ impl AdminRpcHandler { EnumerationOrder::Forward, ) .await?; + Ok(AdminRpc::BucketList(buckets)) } @@ -104,6 +111,15 @@ impl AdminRpcHandler { .get_existing_bucket(bucket_id) .await?; + let counters = self + .garage + .object_counter_table + .table + .get(&bucket_id, &EmptyKey) + .await? + .map(|x| x.filtered_values(&self.garage.system.ring.borrow())) + .unwrap_or_default(); + let mut relevant_keys = HashMap::new(); for (k, _) in bucket .state @@ -139,7 +155,11 @@ impl AdminRpcHandler { } } - Ok(AdminRpc::BucketInfo(bucket, relevant_keys)) + Ok(AdminRpc::BucketInfo { + bucket, + relevant_keys, + counters, + }) } #[allow(clippy::ptr_arg)] @@ -431,6 +451,60 @@ impl AdminRpcHandler { Ok(AdminRpc::Ok(msg)) } + async fn handle_bucket_set_quotas(&self, query: &SetQuotasOpt) -> Result { + let bucket_id = self + .garage + .bucket_helper() + .resolve_global_bucket_name(&query.bucket) + .await? + .ok_or_bad_request("Bucket not found")?; + + let mut bucket = self + .garage + .bucket_helper() + .get_existing_bucket(bucket_id) + .await?; + let bucket_state = bucket.state.as_option_mut().unwrap(); + + if query.max_size.is_none() && query.max_objects.is_none() { + return Err(Error::BadRequest( + "You must specify either --max-size or --max-objects (or both) for this command to do something.".to_string(), + )); + } + + let mut quotas = bucket_state.quotas.get().clone(); + + match query.max_size.as_ref().map(String::as_ref) { + Some("none") => quotas.max_size = None, + Some(v) => { + let bs = v + .parse::() + .ok_or_bad_request(format!("Invalid size specified: {}", v))?; + quotas.max_size = Some(bs.as_u64()); + } + _ => (), + } + + match query.max_objects.as_ref().map(String::as_ref) { + Some("none") => quotas.max_objects = None, + Some(v) => { + let mo = v + .parse::() + .ok_or_bad_request(format!("Invalid number specified: {}", v))?; + quotas.max_objects = Some(mo); + } + _ => (), + } + + bucket_state.quotas.update(quotas); + self.garage.bucket_table.insert(&bucket).await?; + + Ok(AdminRpc::Ok(format!( + "Quotas updated for {}", + &query.bucket + ))) + } + async fn handle_key_cmd(&self, cmd: &KeyOperation) -> Result { match cmd { KeyOperation::List => self.handle_list_keys().await, @@ -619,7 +693,7 @@ impl AdminRpcHandler { ))) } } else { - let repair = Repair { + let repair = OnlineRepair { garage: self.garage.clone(), }; self.garage diff --git a/src/garage/cli/cmd.rs b/src/garage/cli/cmd.rs index b2dd8f14..3a0bd956 100644 --- a/src/garage/cli/cmd.rs +++ b/src/garage/cli/cmd.rs @@ -169,8 +169,12 @@ pub async fn cmd_admin( AdminRpc::BucketList(bl) => { print_bucket_list(bl); } - AdminRpc::BucketInfo(bucket, rk) => { - print_bucket_info(&bucket, &rk); + AdminRpc::BucketInfo { + bucket, + relevant_keys, + counters, + } => { + print_bucket_info(&bucket, &relevant_keys, &counters); } AdminRpc::KeyList(kl) => { print_key_list(kl); diff --git a/src/garage/cli/structs.rs b/src/garage/cli/structs.rs index a0c49aeb..4f2efe19 100644 --- a/src/garage/cli/structs.rs +++ b/src/garage/cli/structs.rs @@ -33,10 +33,15 @@ pub enum Command { #[structopt(name = "migrate")] Migrate(MigrateOpt), - /// Start repair of node data + /// Start repair of node data on remote node #[structopt(name = "repair")] Repair(RepairOpt), + /// Offline reparation of node data (these repairs must be run offline + /// directly on the server node) + #[structopt(name = "offline-repair")] + OfflineRepair(OfflineRepairOpt), + /// Gather node statistics #[structopt(name = "stats")] Stats(StatsOpt), @@ -175,6 +180,10 @@ pub enum BucketOperation { /// Expose as website or not #[structopt(name = "website")] Website(WebsiteOpt), + + /// Set the quotas for this bucket + #[structopt(name = "set-quotas")] + SetQuotas(SetQuotasOpt), } #[derive(Serialize, Deserialize, StructOpt, Debug)] @@ -261,6 +270,21 @@ pub struct PermBucketOpt { pub bucket: String, } +#[derive(Serialize, Deserialize, StructOpt, Debug)] +pub struct SetQuotasOpt { + /// Bucket name + pub bucket: String, + + /// Set a maximum size for the bucket (specify a size e.g. in MiB or GiB, + /// or `none` for no size restriction) + #[structopt(long = "max-size")] + pub max_size: Option, + + /// Set a maximum number of objects for the bucket (or `none` for no restriction) + #[structopt(long = "max-objects")] + pub max_objects: Option, +} + #[derive(Serialize, Deserialize, StructOpt, Debug)] pub enum KeyOperation { /// List keys @@ -405,6 +429,27 @@ pub enum RepairWhat { }, } +#[derive(Serialize, Deserialize, StructOpt, Debug, Clone)] +pub struct OfflineRepairOpt { + /// Confirm the launch of the repair operation + #[structopt(long = "yes")] + pub yes: bool, + + #[structopt(subcommand)] + pub what: OfflineRepairWhat, +} + +#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)] +pub enum OfflineRepairWhat { + /// Repair K2V item counters + #[cfg(feature = "k2v")] + #[structopt(name = "k2v_item_counters")] + K2VItemCounters, + /// Repair object counters + #[structopt(name = "object_counters")] + ObjectCounters, +} + #[derive(Serialize, Deserialize, StructOpt, Debug, Clone)] pub struct StatsOpt { /// Gather statistics from all nodes diff --git a/src/garage/cli/util.rs b/src/garage/cli/util.rs index 6d73be3a..329e8a3e 100644 --- a/src/garage/cli/util.rs +++ b/src/garage/cli/util.rs @@ -7,6 +7,7 @@ use garage_util::formater::format_table; use garage_model::bucket_table::*; use garage_model::key_table::*; +use garage_model::s3::object_table::{BYTES, OBJECTS, UNFINISHED_UPLOADS}; pub fn print_bucket_list(bl: Vec) { println!("List of buckets:"); @@ -29,11 +30,12 @@ pub fn print_bucket_list(bl: Vec) { [((k, n), _, _)] => format!("{}:{}", k, n), s => format!("[{} local aliases]", s.len()), }; + table.push(format!( "\t{}\t{}\t{}", aliases.join(","), local_aliases_n, - hex::encode(bucket.id) + hex::encode(bucket.id), )); } format_table(table); @@ -121,7 +123,11 @@ pub fn print_key_info(key: &Key, relevant_buckets: &HashMap) { } } -pub fn print_bucket_info(bucket: &Bucket, relevant_keys: &HashMap) { +pub fn print_bucket_info( + bucket: &Bucket, + relevant_keys: &HashMap, + counters: &HashMap, +) { let key_name = |k| { relevant_keys .get(k) @@ -133,7 +139,42 @@ pub fn print_bucket_info(bucket: &Bucket, relevant_keys: &HashMap) match &bucket.state { Deletable::Deleted => println!("Bucket is deleted."), Deletable::Present(p) => { - println!("Website access: {}", p.website_config.get().is_some()); + let size = + bytesize::ByteSize::b(counters.get(BYTES).cloned().unwrap_or_default() as u64); + println!( + "\nSize: {} ({})", + size.to_string_as(true), + size.to_string_as(false) + ); + println!( + "Objects: {}", + counters.get(OBJECTS).cloned().unwrap_or_default() + ); + println!( + "Unfinished multipart uploads: {}", + counters + .get(UNFINISHED_UPLOADS) + .cloned() + .unwrap_or_default() + ); + + println!("\nWebsite access: {}", p.website_config.get().is_some()); + + let quotas = p.quotas.get(); + if quotas.max_size.is_some() || quotas.max_objects.is_some() { + println!("\nQuotas:"); + if let Some(ms) = quotas.max_size { + let ms = bytesize::ByteSize::b(ms); + println!( + " maximum size: {} ({})", + ms.to_string_as(true), + ms.to_string_as(false) + ); + } + if let Some(mo) = quotas.max_objects { + println!(" maximum number of objects: {}", mo); + } + } println!("\nGlobal aliases:"); for (alias, _, active) in p.aliases.items().iter() { diff --git a/src/garage/main.rs b/src/garage/main.rs index bd09b6ea..3fa5c3c0 100644 --- a/src/garage/main.rs +++ b/src/garage/main.rs @@ -61,17 +61,17 @@ async fn main() { pretty_env_logger::init(); sodiumoxide::init().expect("Unable to init sodiumoxide"); + // Abort on panic (same behavior as in Go) + std::panic::set_hook(Box::new(|panic_info| { + error!("{}", panic_info.to_string()); + std::process::abort(); + })); + let opt = Opt::from_args(); - let res = match opt.cmd { - Command::Server => { - // Abort on panic (same behavior as in Go) - std::panic::set_hook(Box::new(|panic_info| { - error!("{}", panic_info.to_string()); - std::process::abort(); - })); - - server::run_server(opt.config_file).await + Command::Server => server::run_server(opt.config_file).await, + Command::OfflineRepair(repair_opt) => { + repair::offline::offline_repair(opt.config_file, repair_opt).await } Command::Node(NodeOperation::NodeId(node_id_opt)) => { node_id_command(opt.config_file, node_id_opt.quiet) diff --git a/src/garage/repair/mod.rs b/src/garage/repair/mod.rs new file mode 100644 index 00000000..4699ace5 --- /dev/null +++ b/src/garage/repair/mod.rs @@ -0,0 +1,2 @@ +pub mod offline; +pub mod online; diff --git a/src/garage/repair/offline.rs b/src/garage/repair/offline.rs new file mode 100644 index 00000000..7760a8bd --- /dev/null +++ b/src/garage/repair/offline.rs @@ -0,0 +1,55 @@ +use std::path::PathBuf; + +use tokio::sync::watch; + +use garage_util::background::*; +use garage_util::config::*; +use garage_util::error::*; + +use garage_model::garage::Garage; + +use crate::cli::structs::*; + +pub async fn offline_repair(config_file: PathBuf, opt: OfflineRepairOpt) -> Result<(), Error> { + if !opt.yes { + return Err(Error::Message( + "Please add the --yes flag to launch repair operation".into(), + )); + } + + info!("Loading configuration..."); + let config = read_config(config_file)?; + + info!("Initializing background runner..."); + let (done_tx, done_rx) = watch::channel(false); + let (background, await_background_done) = BackgroundRunner::new(16, done_rx); + + info!("Initializing Garage main data store..."); + let garage = Garage::new(config.clone(), background)?; + + info!("Launching repair operation..."); + match opt.what { + #[cfg(feature = "k2v")] + OfflineRepairWhat::K2VItemCounters => { + garage + .k2v + .counter_table + .offline_recount_all(&garage.k2v.item_table)?; + } + OfflineRepairWhat::ObjectCounters => { + garage + .object_counter_table + .offline_recount_all(&garage.object_table)?; + } + } + + info!("Repair operation finished, shutting down Garage internals..."); + done_tx.send(true).unwrap(); + drop(garage); + + await_background_done.await?; + + info!("Cleaning up..."); + + Ok(()) +} diff --git a/src/garage/repair.rs b/src/garage/repair/online.rs similarity index 98% rename from src/garage/repair.rs rename to src/garage/repair/online.rs index 17e14b8b..d6a71742 100644 --- a/src/garage/repair.rs +++ b/src/garage/repair/online.rs @@ -11,11 +11,11 @@ use garage_util::error::Error; use crate::*; -pub struct Repair { +pub struct OnlineRepair { pub garage: Arc, } -impl Repair { +impl OnlineRepair { pub async fn repair_worker(&self, opt: RepairOpt, must_exit: watch::Receiver) { if let Err(e) = self.repair_worker_aux(opt, must_exit).await { warn!("Repair worker failed with error: {}", e); diff --git a/src/garage/server.rs b/src/garage/server.rs index 7aa6185f..6321357a 100644 --- a/src/garage/server.rs +++ b/src/garage/server.rs @@ -2,8 +2,6 @@ use std::path::PathBuf; use tokio::sync::watch; -use garage_db as db; - use garage_util::background::*; use garage_util::config::*; use garage_util::error::Error; @@ -29,57 +27,14 @@ async fn wait_from(mut chan: watch::Receiver) { pub async fn run_server(config_file: PathBuf) -> Result<(), Error> { info!("Loading configuration..."); - let config = read_config(config_file).expect("Unable to read config file"); - - info!("Opening database..."); - let mut db_path = config.metadata_dir.clone(); - std::fs::create_dir_all(&db_path).expect("Unable to create Garage meta data directory"); - let db = match config.db_engine.as_str() { - "sled" => { - db_path.push("db"); - info!("Opening Sled database at: {}", db_path.display()); - let db = db::sled_adapter::sled::Config::default() - .path(&db_path) - .cache_capacity(config.sled_cache_capacity) - .flush_every_ms(Some(config.sled_flush_every_ms)) - .open() - .expect("Unable to open sled DB"); - db::sled_adapter::SledDb::init(db) - } - "sqlite" | "sqlite3" | "rusqlite" => { - db_path.push("db.sqlite"); - info!("Opening Sqlite database at: {}", db_path.display()); - let db = db::sqlite_adapter::rusqlite::Connection::open(db_path) - .expect("Unable to open sqlite DB"); - db::sqlite_adapter::SqliteDb::init(db) - } - "lmdb" | "heed" => { - db_path.push("db.lmdb"); - info!("Opening LMDB database at: {}", db_path.display()); - std::fs::create_dir_all(&db_path).expect("Unable to create LMDB data directory"); - let map_size = garage_db::lmdb_adapter::recommended_map_size(); - - let db = db::lmdb_adapter::heed::EnvOpenOptions::new() - .max_dbs(100) - .map_size(map_size) - .open(&db_path) - .expect("Unable to open LMDB DB"); - db::lmdb_adapter::LmdbDb::init(db) - } - e => { - return Err(Error::Message(format!( - "Unsupported DB engine: {} (options: sled, sqlite, lmdb)", - e - ))); - } - }; + let config = read_config(config_file)?; info!("Initializing background runner..."); let watch_cancel = netapp::util::watch_ctrl_c(); let (background, await_background_done) = BackgroundRunner::new(16, watch_cancel.clone()); info!("Initializing Garage main data store..."); - let garage = Garage::new(config.clone(), db, background); + let garage = Garage::new(config.clone(), background)?; info!("Initialize tracing..."); if let Some(export_to) = config.admin.trace_sink { @@ -89,6 +44,7 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> { info!("Initialize Admin API server and metrics collector..."); let admin_server = AdminApiServer::new(garage.clone()); + info!("Launching internal Garage cluster communications..."); let run_system = tokio::spawn(garage.system.clone().run(watch_cancel.clone())); info!("Create admin RPC handler..."); diff --git a/src/model/bucket_table.rs b/src/model/bucket_table.rs index 7c7b9f30..130eb6a6 100644 --- a/src/model/bucket_table.rs +++ b/src/model/bucket_table.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use garage_table::crdt::Crdt; +use garage_table::crdt::*; use garage_table::*; use garage_util::data::*; use garage_util::time::*; @@ -44,6 +44,9 @@ pub struct BucketParams { pub website_config: crdt::Lww>, /// CORS rules pub cors_config: crdt::Lww>>, + /// Bucket quotas + #[serde(default)] + pub quotas: crdt::Lww, } #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] @@ -62,6 +65,18 @@ pub struct CorsRule { pub expose_headers: Vec, } +#[derive(Default, PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Serialize, Deserialize)] +pub struct BucketQuotas { + /// Maximum size in bytes (bucket size = sum of sizes of objects in the bucket) + pub max_size: Option, + /// Maximum number of non-deleted objects in the bucket + pub max_objects: Option, +} + +impl AutoCrdt for BucketQuotas { + const WARN_IF_DIFFERENT: bool = true; +} + impl BucketParams { /// Create an empty BucketParams with no authorized keys and no website accesss pub fn new() -> Self { @@ -72,6 +87,7 @@ impl BucketParams { local_aliases: crdt::LwwMap::new(), website_config: crdt::Lww::new(None), cors_config: crdt::Lww::new(None), + quotas: crdt::Lww::new(BucketQuotas::default()), } } } @@ -86,6 +102,7 @@ impl Crdt for BucketParams { self.website_config.merge(&o.website_config); self.cors_config.merge(&o.cors_config); + self.quotas.merge(&o.quotas); } } diff --git a/src/model/garage.rs b/src/model/garage.rs index 280f3dc7..15769a17 100644 --- a/src/model/garage.rs +++ b/src/model/garage.rs @@ -6,6 +6,7 @@ use garage_db as db; use garage_util::background::*; use garage_util::config::*; +use garage_util::error::Error; use garage_rpc::system::System; @@ -22,12 +23,11 @@ use crate::s3::version_table::*; use crate::bucket_alias_table::*; use crate::bucket_table::*; use crate::helper; +use crate::index_counter::*; use crate::key_table::*; #[cfg(feature = "k2v")] -use crate::index_counter::*; -#[cfg(feature = "k2v")] -use crate::k2v::{counter_table::*, item_table::*, poll::*, rpc::*}; +use crate::k2v::{item_table::*, poll::*, rpc::*}; /// An entire Garage full of data pub struct Garage { @@ -52,6 +52,8 @@ pub struct Garage { /// Table containing S3 objects pub object_table: Arc>, + /// Counting table containing object counters + pub object_counter_table: Arc>, /// Table containing S3 object versions pub version_table: Arc>, /// Table containing S3 block references (not blocks themselves) @@ -66,14 +68,57 @@ pub struct GarageK2V { /// Table containing K2V items pub item_table: Arc>, /// Indexing table containing K2V item counters - pub counter_table: Arc>, + pub counter_table: Arc>, /// K2V RPC handler pub rpc: Arc, } impl Garage { /// Create and run garage - pub fn new(config: Config, db: db::Db, background: Arc) -> Arc { + pub fn new(config: Config, background: Arc) -> Result, Error> { + info!("Opening database..."); + let mut db_path = config.metadata_dir.clone(); + std::fs::create_dir_all(&db_path).expect("Unable to create Garage meta data directory"); + let db = match config.db_engine.as_str() { + "sled" => { + db_path.push("db"); + info!("Opening Sled database at: {}", db_path.display()); + let db = db::sled_adapter::sled::Config::default() + .path(&db_path) + .cache_capacity(config.sled_cache_capacity) + .flush_every_ms(Some(config.sled_flush_every_ms)) + .open() + .expect("Unable to open sled DB"); + db::sled_adapter::SledDb::init(db) + } + "sqlite" | "sqlite3" | "rusqlite" => { + db_path.push("db.sqlite"); + info!("Opening Sqlite database at: {}", db_path.display()); + let db = db::sqlite_adapter::rusqlite::Connection::open(db_path) + .expect("Unable to open sqlite DB"); + db::sqlite_adapter::SqliteDb::init(db) + } + "lmdb" | "heed" => { + db_path.push("db.lmdb"); + info!("Opening LMDB database at: {}", db_path.display()); + std::fs::create_dir_all(&db_path).expect("Unable to create LMDB data directory"); + let map_size = garage_db::lmdb_adapter::recommended_map_size(); + + let db = db::lmdb_adapter::heed::EnvOpenOptions::new() + .max_dbs(100) + .map_size(map_size) + .open(&db_path) + .expect("Unable to open LMDB DB"); + db::lmdb_adapter::LmdbDb::init(db) + } + e => { + return Err(Error::Message(format!( + "Unsupported DB engine: {} (options: sled, sqlite, lmdb)", + e + ))); + } + }; + let network_key = NetworkKey::from_slice( &hex::decode(&config.rpc_secret).expect("Invalid RPC secret key")[..], ) @@ -155,12 +200,16 @@ impl Garage { &db, ); + info!("Initialize object counter table..."); + let object_counter_table = IndexCounter::new(system.clone(), meta_rep_param.clone(), &db); + info!("Initialize object_table..."); #[allow(clippy::redundant_clone)] let object_table = Table::new( ObjectTable { background: background.clone(), version_table: version_table.clone(), + object_counter_table: object_counter_table.clone(), }, meta_rep_param.clone(), system.clone(), @@ -171,9 +220,8 @@ impl Garage { #[cfg(feature = "k2v")] let k2v = GarageK2V::new(system.clone(), &db, meta_rep_param); - info!("Initialize Garage..."); - - Arc::new(Self { + // -- done -- + Ok(Arc::new(Self { config, db, background, @@ -183,11 +231,12 @@ impl Garage { bucket_alias_table, key_table, object_table, + object_counter_table, version_table, block_ref_table, #[cfg(feature = "k2v")] k2v, - }) + })) } pub fn bucket_helper(&self) -> helper::bucket::BucketHelper { diff --git a/src/model/index_counter.rs b/src/model/index_counter.rs index 2602d5d9..36e8172b 100644 --- a/src/model/index_counter.rs +++ b/src/model/index_counter.rs @@ -1,3 +1,4 @@ +use core::ops::Bound; use std::collections::{hash_map, BTreeMap, HashMap}; use std::marker::PhantomData; use std::sync::Arc; @@ -12,30 +13,36 @@ use garage_rpc::ring::Ring; use garage_rpc::system::System; use garage_util::data::*; use garage_util::error::*; +use garage_util::time::*; use garage_table::crdt::*; -use garage_table::replication::TableShardedReplication; +use garage_table::replication::*; use garage_table::*; -pub trait CounterSchema: Clone + PartialEq + Send + Sync + 'static { - const NAME: &'static str; - type P: PartitionKey + Clone + PartialEq + Serialize + for<'de> Deserialize<'de> + Send + Sync; - type S: SortKey + Clone + PartialEq + Serialize + for<'de> Deserialize<'de> + Send + Sync; +pub trait CountedItem: Clone + PartialEq + Send + Sync + 'static { + const COUNTER_TABLE_NAME: &'static str; + + type CP: PartitionKey + Clone + PartialEq + Serialize + for<'de> Deserialize<'de> + Send + Sync; + type CS: SortKey + Clone + PartialEq + Serialize + for<'de> Deserialize<'de> + Send + Sync; + + fn counter_partition_key(&self) -> &Self::CP; + fn counter_sort_key(&self) -> &Self::CS; + fn counts(&self) -> Vec<(&'static str, i64)>; } /// A counter entry in the global table -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] -pub struct CounterEntry { - pub pk: T::P, - pub sk: T::S, +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct CounterEntry { + pub pk: T::CP, + pub sk: T::CS, pub values: BTreeMap, } -impl Entry for CounterEntry { - fn partition_key(&self) -> &T::P { +impl Entry for CounterEntry { + fn partition_key(&self) -> &T::CP { &self.pk } - fn sort_key(&self) -> &T::S { + fn sort_key(&self) -> &T::CS { &self.sk } fn is_tombstone(&self) -> bool { @@ -45,7 +52,7 @@ impl Entry for CounterEntry { } } -impl CounterEntry { +impl CounterEntry { pub fn filtered_values(&self, ring: &Ring) -> HashMap { let nodes = &ring.layout.node_id_vec[..]; self.filtered_values_with_nodes(nodes) @@ -78,7 +85,7 @@ pub struct CounterValue { pub node_values: BTreeMap, } -impl Crdt for CounterEntry { +impl Crdt for CounterEntry { fn merge(&mut self, other: &Self) { for (name, e2) in other.values.iter() { if let Some(e) = self.values.get_mut(name) { @@ -104,15 +111,15 @@ impl Crdt for CounterValue { } } -pub struct CounterTable { +pub struct CounterTable { _phantom_t: PhantomData, } -impl TableSchema for CounterTable { - const TABLE_NAME: &'static str = T::NAME; +impl TableSchema for CounterTable { + const TABLE_NAME: &'static str = T::COUNTER_TABLE_NAME; - type P = T::P; - type S = T::S; + type P = T::CP; + type S = T::CS; type E = CounterEntry; type Filter = (DeletedFilter, Vec); @@ -131,14 +138,14 @@ impl TableSchema for CounterTable { // ---- -pub struct IndexCounter { +pub struct IndexCounter { this_node: Uuid, local_counter: db::Tree, - propagate_tx: mpsc::UnboundedSender<(T::P, T::S, LocalCounterEntry)>, + propagate_tx: mpsc::UnboundedSender<(T::CP, T::CS, LocalCounterEntry)>, pub table: Arc, TableShardedReplication>>, } -impl IndexCounter { +impl IndexCounter { pub fn new( system: Arc, replication: TableShardedReplication, @@ -151,7 +158,7 @@ impl IndexCounter { let this = Arc::new(Self { this_node: system.id, local_counter: db - .open_tree(format!("local_counter:{}", T::NAME)) + .open_tree(format!("local_counter_v2:{}", T::COUNTER_TABLE_NAME)) .expect("Unable to open local counter tree"), propagate_tx, table: Table::new( @@ -166,7 +173,7 @@ impl IndexCounter { let this2 = this.clone(); background.spawn_worker( - format!("{} index counter propagator", T::NAME), + format!("{} index counter propagator", T::COUNTER_TABLE_NAME), move |must_exit| this2.clone().propagate_loop(propagate_rx, must_exit), ); this @@ -175,24 +182,45 @@ impl IndexCounter { pub fn count( &self, tx: &mut db::Transaction, - pk: &T::P, - sk: &T::S, - counts: &[(&str, i64)], + old: Option<&T>, + new: Option<&T>, ) -> db::TxResult<(), Error> { + let pk = old + .map(|e| e.counter_partition_key()) + .unwrap_or_else(|| new.unwrap().counter_partition_key()); + let sk = old + .map(|e| e.counter_sort_key()) + .unwrap_or_else(|| new.unwrap().counter_sort_key()); + + // calculate counter differences + let mut counts = HashMap::new(); + for (k, v) in old.map(|x| x.counts()).unwrap_or_default() { + *counts.entry(k).or_insert(0) -= v; + } + for (k, v) in new.map(|x| x.counts()).unwrap_or_default() { + *counts.entry(k).or_insert(0) += v; + } + + // update local counter table let tree_key = self.table.data.tree_key(pk, sk); let mut entry = match tx.get(&self.local_counter, &tree_key[..])? { - Some(old_bytes) => rmp_serde::decode::from_read_ref::<_, LocalCounterEntry>(&old_bytes) - .map_err(Error::RmpDecode) - .map_err(db::TxError::Abort)?, + Some(old_bytes) => { + rmp_serde::decode::from_read_ref::<_, LocalCounterEntry>(&old_bytes) + .map_err(Error::RmpDecode) + .map_err(db::TxError::Abort)? + } None => LocalCounterEntry { + pk: pk.clone(), + sk: sk.clone(), values: BTreeMap::new(), }, }; + let now = now_msec(); for (s, inc) in counts.iter() { let mut ent = entry.values.entry(s.to_string()).or_insert((0, 0)); - ent.0 += 1; + ent.0 = std::cmp::max(ent.0 + 1, now); ent.1 += *inc; } @@ -213,7 +241,7 @@ impl IndexCounter { async fn propagate_loop( self: Arc, - mut propagate_rx: mpsc::UnboundedReceiver<(T::P, T::S, LocalCounterEntry)>, + mut propagate_rx: mpsc::UnboundedReceiver<(T::CP, T::CS, LocalCounterEntry)>, must_exit: watch::Receiver, ) { // This loop batches updates to counters to be sent all at once. @@ -236,7 +264,7 @@ impl IndexCounter { if let Some((pk, sk, counters)) = ent { let tree_key = self.table.data.tree_key(&pk, &sk); - let dist_entry = counters.into_counter_entry::(self.this_node, pk, sk); + let dist_entry = counters.into_counter_entry(self.this_node); match buf.entry(tree_key) { hash_map::Entry::Vacant(e) => { e.insert(dist_entry); @@ -255,10 +283,10 @@ impl IndexCounter { if let Err(e) = self.table.insert_many(entries).await { errors += 1; if errors >= 2 && *must_exit.borrow() { - error!("({}) Could not propagate {} counter values: {}, these counters will not be updated correctly.", T::NAME, buf.len(), e); + error!("({}) Could not propagate {} counter values: {}, these counters will not be updated correctly.", T::COUNTER_TABLE_NAME, buf.len(), e); break; } - warn!("({}) Could not propagate {} counter values: {}, retrying in 5 seconds (retry #{})", T::NAME, buf.len(), e, errors); + warn!("({}) Could not propagate {} counter values: {}, retrying in 5 seconds (retry #{})", T::COUNTER_TABLE_NAME, buf.len(), e, errors); tokio::time::sleep(Duration::from_secs(5)).await; continue; } @@ -272,23 +300,155 @@ impl IndexCounter { } } } + + pub fn offline_recount_all( + &self, + counted_table: &Arc>, + ) -> Result<(), Error> + where + TS: TableSchema, + TR: TableReplication, + { + let save_counter_entry = |entry: CounterEntry| -> Result<(), Error> { + let entry_k = self + .table + .data + .tree_key(entry.partition_key(), entry.sort_key()); + self.table + .data + .update_entry_with(&entry_k, |ent| match ent { + Some(mut ent) => { + ent.merge(&entry); + ent + } + None => entry.clone(), + })?; + Ok(()) + }; + + // 1. Set all old local counters to zero + let now = now_msec(); + let mut next_start: Option> = None; + loop { + let low_bound = match next_start.take() { + Some(v) => Bound::Excluded(v), + None => Bound::Unbounded, + }; + let mut batch = vec![]; + for item in self.local_counter.range((low_bound, Bound::Unbounded))? { + batch.push(item?); + if batch.len() > 1000 { + break; + } + } + + if batch.is_empty() { + break; + } + + info!("zeroing old counters... ({})", hex::encode(&batch[0].0)); + for (local_counter_k, local_counter) in batch { + let mut local_counter = + rmp_serde::decode::from_read_ref::<_, LocalCounterEntry>(&local_counter)?; + + for (_, tv) in local_counter.values.iter_mut() { + tv.0 = std::cmp::max(tv.0 + 1, now); + tv.1 = 0; + } + + let local_counter_bytes = rmp_to_vec_all_named(&local_counter)?; + self.local_counter + .insert(&local_counter_k, &local_counter_bytes)?; + + let counter_entry = local_counter.into_counter_entry(self.this_node); + save_counter_entry(counter_entry)?; + + next_start = Some(local_counter_k); + } + } + + // 2. Recount all table entries + let now = now_msec(); + let mut next_start: Option> = None; + loop { + let low_bound = match next_start.take() { + Some(v) => Bound::Excluded(v), + None => Bound::Unbounded, + }; + let mut batch = vec![]; + for item in counted_table + .data + .store + .range((low_bound, Bound::Unbounded))? + { + batch.push(item?); + if batch.len() > 1000 { + break; + } + } + + if batch.is_empty() { + break; + } + + info!("counting entries... ({})", hex::encode(&batch[0].0)); + for (counted_entry_k, counted_entry) in batch { + let counted_entry = counted_table.data.decode_entry(&counted_entry)?; + + let pk = counted_entry.counter_partition_key(); + let sk = counted_entry.counter_sort_key(); + let counts = counted_entry.counts(); + + let local_counter_key = self.table.data.tree_key(pk, sk); + let mut local_counter = match self.local_counter.get(&local_counter_key)? { + Some(old_bytes) => { + let ent = rmp_serde::decode::from_read_ref::<_, LocalCounterEntry>( + &old_bytes, + )?; + assert!(ent.pk == *pk); + assert!(ent.sk == *sk); + ent + } + None => LocalCounterEntry { + pk: pk.clone(), + sk: sk.clone(), + values: BTreeMap::new(), + }, + }; + for (s, v) in counts.iter() { + let mut tv = local_counter.values.entry(s.to_string()).or_insert((0, 0)); + tv.0 = std::cmp::max(tv.0 + 1, now); + tv.1 += v; + } + + let local_counter_bytes = rmp_to_vec_all_named(&local_counter)?; + self.local_counter + .insert(&local_counter_key, local_counter_bytes)?; + + let counter_entry = local_counter.into_counter_entry(self.this_node); + save_counter_entry(counter_entry)?; + + next_start = Some(counted_entry_k); + } + } + + // Done + Ok(()) + } } #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] -struct LocalCounterEntry { +struct LocalCounterEntry { + pk: T::CP, + sk: T::CS, values: BTreeMap, } -impl LocalCounterEntry { - fn into_counter_entry( - self, - this_node: Uuid, - pk: T::P, - sk: T::S, - ) -> CounterEntry { +impl LocalCounterEntry { + fn into_counter_entry(self, this_node: Uuid) -> CounterEntry { CounterEntry { - pk, - sk, + pk: self.pk, + sk: self.sk, values: self .values .into_iter() diff --git a/src/model/k2v/counter_table.rs b/src/model/k2v/counter_table.rs deleted file mode 100644 index 4856eb2b..00000000 --- a/src/model/k2v/counter_table.rs +++ /dev/null @@ -1,20 +0,0 @@ -use garage_util::data::*; - -use crate::index_counter::*; - -pub const ENTRIES: &str = "entries"; -pub const CONFLICTS: &str = "conflicts"; -pub const VALUES: &str = "values"; -pub const BYTES: &str = "bytes"; - -#[derive(PartialEq, Clone)] -pub struct K2VCounterTable; - -impl CounterSchema for K2VCounterTable { - const NAME: &'static str = "k2v_index_counter"; - - // Partition key = bucket id - type P = Uuid; - // Sort key = K2V item's partition key - type S = String; -} diff --git a/src/model/k2v/item_table.rs b/src/model/k2v/item_table.rs index 991fe66d..baa1db4b 100644 --- a/src/model/k2v/item_table.rs +++ b/src/model/k2v/item_table.rs @@ -10,9 +10,13 @@ use garage_table::*; use crate::index_counter::*; use crate::k2v::causality::*; -use crate::k2v::counter_table::*; use crate::k2v::poll::*; +pub const ENTRIES: &str = "entries"; +pub const CONFLICTS: &str = "conflicts"; +pub const VALUES: &str = "values"; +pub const BYTES: &str = "bytes"; + #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub struct K2VItem { pub partition: K2VItemPartition, @@ -112,27 +116,6 @@ impl K2VItem { ent.discard(); } } - - // returns counters: (non-deleted entries, conflict entries, non-tombstone values, bytes used) - fn stats(&self) -> (i64, i64, i64, i64) { - let values = self.values(); - - let n_entries = if self.is_tombstone() { 0 } else { 1 }; - let n_conflicts = if values.len() > 1 { 1 } else { 0 }; - let n_values = values - .iter() - .filter(|v| matches!(v, DvvsValue::Value(_))) - .count() as i64; - let n_bytes = values - .iter() - .map(|v| match v { - DvvsValue::Deleted => 0, - DvvsValue::Value(v) => v.len() as i64, - }) - .sum(); - - (n_entries, n_conflicts, n_values, n_bytes) - } } impl DvvsEntry { @@ -204,7 +187,7 @@ impl Entry for K2VItem { } pub struct K2VItemTable { - pub(crate) counter_table: Arc>, + pub(crate) counter_table: Arc>, pub(crate) subscriptions: Arc, } @@ -229,40 +212,14 @@ impl TableSchema for K2VItemTable { new: Option<&Self::E>, ) -> db::TxOpResult<()> { // 1. Count - let (old_entries, old_conflicts, old_values, old_bytes) = match old { - None => (0, 0, 0, 0), - Some(e) => e.stats(), - }; - let (new_entries, new_conflicts, new_values, new_bytes) = match new { - None => (0, 0, 0, 0), - Some(e) => e.stats(), - }; - - let count_pk = old - .map(|e| e.partition.bucket_id) - .unwrap_or_else(|| new.unwrap().partition.bucket_id); - let count_sk = old - .map(|e| &e.partition.partition_key) - .unwrap_or_else(|| &new.unwrap().partition.partition_key); - - let counter_res = self.counter_table.count( - tx, - &count_pk, - count_sk, - &[ - (ENTRIES, new_entries - old_entries), - (CONFLICTS, new_conflicts - old_conflicts), - (VALUES, new_values - old_values), - (BYTES, new_bytes - old_bytes), - ], - ); + let counter_res = self.counter_table.count(tx, old, new); if let Err(e) = db::unabort(counter_res)? { // This result can be returned by `counter_table.count()` for instance // if messagepack serialization or deserialization fails at some step. // Warn admin but ignore this error for now, that's all we can do. error!( - "Unable to update K2V item counter for bucket {:?} partition {}: {}. Index values will be wrong!", - count_pk, count_sk, e + "Unable to update K2V item counter: {}. Index values will be wrong!", + e ); } @@ -282,6 +239,47 @@ impl TableSchema for K2VItemTable { } } +impl CountedItem for K2VItem { + const COUNTER_TABLE_NAME: &'static str = "k2v_index_counter_v2"; + + // Partition key = bucket id + type CP = Uuid; + // Sort key = K2V item's partition key + type CS = String; + + fn counter_partition_key(&self) -> &Uuid { + &self.partition.bucket_id + } + fn counter_sort_key(&self) -> &String { + &self.partition.partition_key + } + + fn counts(&self) -> Vec<(&'static str, i64)> { + let values = self.values(); + + let n_entries = if self.is_tombstone() { 0 } else { 1 }; + let n_conflicts = if values.len() > 1 { 1 } else { 0 }; + let n_values = values + .iter() + .filter(|v| matches!(v, DvvsValue::Value(_))) + .count() as i64; + let n_bytes = values + .iter() + .map(|v| match v { + DvvsValue::Deleted => 0, + DvvsValue::Value(v) => v.len() as i64, + }) + .sum(); + + vec![ + (ENTRIES, n_entries), + (CONFLICTS, n_conflicts), + (VALUES, n_values), + (BYTES, n_bytes), + ] + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/model/k2v/mod.rs b/src/model/k2v/mod.rs index 664172a6..f6a96151 100644 --- a/src/model/k2v/mod.rs +++ b/src/model/k2v/mod.rs @@ -1,6 +1,5 @@ pub mod causality; -pub mod counter_table; pub mod item_table; pub mod poll; diff --git a/src/model/migrate.rs b/src/model/migrate.rs index 25acb4b0..5fc67069 100644 --- a/src/model/migrate.rs +++ b/src/model/migrate.rs @@ -77,6 +77,7 @@ impl Migrate { local_aliases: LwwMap::new(), website_config: Lww::new(website), cors_config: Lww::new(None), + quotas: Lww::new(Default::default()), }), }) .await?; diff --git a/src/model/s3/object_table.rs b/src/model/s3/object_table.rs index 62f5d8d9..a3914c36 100644 --- a/src/model/s3/object_table.rs +++ b/src/model/s3/object_table.rs @@ -11,10 +11,15 @@ use garage_table::crdt::*; use garage_table::replication::TableShardedReplication; use garage_table::*; +use crate::index_counter::*; use crate::s3::version_table::*; use garage_model_050::object_table as old; +pub const OBJECTS: &str = "objects"; +pub const UNFINISHED_UPLOADS: &str = "unfinished_uploads"; +pub const BYTES: &str = "bytes"; + /// An object #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub struct Object { @@ -218,6 +223,7 @@ impl Crdt for Object { pub struct ObjectTable { pub background: Arc, pub version_table: Arc>, + pub object_counter_table: Arc>, } #[derive(Clone, Copy, Debug, Serialize, Deserialize)] @@ -236,10 +242,20 @@ impl TableSchema for ObjectTable { fn updated( &self, - _tx: &mut db::Transaction, + tx: &mut db::Transaction, old: Option<&Self::E>, new: Option<&Self::E>, ) -> db::TxOpResult<()> { + // 1. Count + let counter_res = self.object_counter_table.count(tx, old, new); + if let Err(e) = db::unabort(counter_res)? { + error!( + "Unable to update object counter: {}. Index values will be wrong!", + e + ); + } + + // 2. Spawn threads that propagates deletions to version table let version_table = self.version_table.clone(); let old = old.cloned(); let new = new.cloned(); @@ -283,6 +299,49 @@ impl TableSchema for ObjectTable { } } +impl CountedItem for Object { + const COUNTER_TABLE_NAME: &'static str = "bucket_object_counter"; + + // Partition key = bucket id + type CP = Uuid; + // Sort key = nothing + type CS = EmptyKey; + + fn counter_partition_key(&self) -> &Uuid { + &self.bucket_id + } + fn counter_sort_key(&self) -> &EmptyKey { + &EmptyKey + } + + fn counts(&self) -> Vec<(&'static str, i64)> { + let versions = self.versions(); + let n_objects = if versions.iter().any(|v| v.is_data()) { + 1 + } else { + 0 + }; + let n_unfinished_uploads = versions + .iter() + .filter(|v| matches!(v.state, ObjectVersionState::Uploading(_))) + .count(); + let n_bytes = versions + .iter() + .map(|v| match &v.state { + ObjectVersionState::Complete(ObjectVersionData::Inline(meta, _)) + | ObjectVersionState::Complete(ObjectVersionData::FirstBlock(meta, _)) => meta.size, + _ => 0, + }) + .sum::(); + + vec![ + (OBJECTS, n_objects), + (UNFINISHED_UPLOADS, n_unfinished_uploads as i64), + (BYTES, n_bytes as i64), + ] + } +} + // vvvvvvvv migration code, stupid stuff vvvvvvvvvvvv // (we just want to change bucket into bucket_id by hashing it) From 996f2a6d585a31f7cd61253e1da076bf8b41f6d6 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 23 Jun 2022 14:28:40 +0200 Subject: [PATCH 025/149] Slides for talk at IMT Atlantique / STACK on 2022-06-23 (#333) Co-authored-by: Alex Auvolat Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/333 Co-authored-by: Alex Co-committed-by: Alex --- doc/talks/2022-06-23-stack/.gitignore | 14 + doc/talks/2022-06-23-stack/Makefile | 5 + .../2022-06-23-stack/assets/AGPLv3_Logo.png | Bin 0 -> 32497 bytes .../2022-06-23-stack/assets/aerogramme.png | Bin 0 -> 117865 bytes .../2022-06-23-stack/assets/aerogramme.svg | 1241 +++++ .../assets/aerogramme_components1.drawio.pdf | 3 + .../assets/aerogramme_components1.png | Bin 0 -> 26898 bytes .../assets/aerogramme_components2.drawio.pdf | 3 + .../assets/aerogramme_components2.png | Bin 0 -> 27405 bytes .../assets/aerogramme_datatype.drawio.pdf | 3 + .../assets/aerogramme_datatype.png | Bin 0 -> 9090 bytes .../assets/aerogramme_keys.drawio.pdf | 3 + .../assets/aerogramme_keys.png | Bin 0 -> 17869 bytes doc/talks/2022-06-23-stack/assets/alex.jpg | Bin 0 -> 4914 bytes doc/talks/2022-06-23-stack/assets/atuin.jpg | Bin 0 -> 269747 bytes .../2022-06-23-stack/assets/compatibility.png | Bin 0 -> 84505 bytes .../assets/consistent_hashing_1.svg | 301 ++ .../assets/consistent_hashing_2.svg | 334 ++ .../assets/consistent_hashing_3.svg | 358 ++ .../assets/consistent_hashing_4.svg | 377 ++ .../2022-06-23-stack/assets/deuxfleurs.svg | 91 + .../assets/endpoint-latency-dc.png | Bin 0 -> 131776 bytes .../2022-06-23-stack/assets/garage.drawio.pdf | 3 + .../2022-06-23-stack/assets/garage.drawio.png | Bin 0 -> 13463 bytes .../assets/garage2.drawio.png | Bin 0 -> 89618 bytes .../assets/garage2a.drawio.pdf | 3 + .../assets/garage2b.drawio.pdf | 3 + .../2022-06-23-stack/assets/garage_tables.svg | 537 ++ .../2022-06-23-stack/assets/garageuses.png | Bin 0 -> 53251 bytes .../2022-06-23-stack/assets/inframap.jpg | Bin 0 -> 38247 bytes .../assets/location-aware.png | Bin 0 -> 99269 bytes .../2022-06-23-stack/assets/logo_chatons.png | Bin 0 -> 203533 bytes doc/talks/2022-06-23-stack/assets/map.png | Bin 0 -> 148270 bytes doc/talks/2022-06-23-stack/assets/minio.png | Bin 0 -> 13497 bytes doc/talks/2022-06-23-stack/assets/neptune.jpg | Bin 0 -> 177936 bytes doc/talks/2022-06-23-stack/assets/quentin.jpg | Bin 0 -> 39221 bytes .../2022-06-23-stack/assets/rust_logo.png | Bin 0 -> 14835 bytes doc/talks/2022-06-23-stack/assets/slide1.png | Bin 0 -> 89059 bytes doc/talks/2022-06-23-stack/assets/slide2.png | Bin 0 -> 83364 bytes doc/talks/2022-06-23-stack/assets/slide3.png | Bin 0 -> 127275 bytes doc/talks/2022-06-23-stack/assets/slideB1.png | Bin 0 -> 86072 bytes doc/talks/2022-06-23-stack/assets/slideB2.png | Bin 0 -> 83399 bytes doc/talks/2022-06-23-stack/assets/slideB3.png | Bin 0 -> 82581 bytes doc/talks/2022-06-23-stack/assets/slides.svg | 4326 +++++++++++++++++ doc/talks/2022-06-23-stack/assets/slidesB.svg | 444 ++ doc/talks/2022-06-23-stack/talk.pdf | 3 + doc/talks/2022-06-23-stack/talk.tex | 480 ++ 47 files changed, 8532 insertions(+) create mode 100644 doc/talks/2022-06-23-stack/.gitignore create mode 100644 doc/talks/2022-06-23-stack/Makefile create mode 100644 doc/talks/2022-06-23-stack/assets/AGPLv3_Logo.png create mode 100644 doc/talks/2022-06-23-stack/assets/aerogramme.png create mode 100644 doc/talks/2022-06-23-stack/assets/aerogramme.svg create mode 100644 doc/talks/2022-06-23-stack/assets/aerogramme_components1.drawio.pdf create mode 100644 doc/talks/2022-06-23-stack/assets/aerogramme_components1.png create mode 100644 doc/talks/2022-06-23-stack/assets/aerogramme_components2.drawio.pdf create mode 100644 doc/talks/2022-06-23-stack/assets/aerogramme_components2.png create mode 100644 doc/talks/2022-06-23-stack/assets/aerogramme_datatype.drawio.pdf create mode 100644 doc/talks/2022-06-23-stack/assets/aerogramme_datatype.png create mode 100644 doc/talks/2022-06-23-stack/assets/aerogramme_keys.drawio.pdf create mode 100644 doc/talks/2022-06-23-stack/assets/aerogramme_keys.png create mode 100644 doc/talks/2022-06-23-stack/assets/alex.jpg create mode 100644 doc/talks/2022-06-23-stack/assets/atuin.jpg create mode 100644 doc/talks/2022-06-23-stack/assets/compatibility.png create mode 100644 doc/talks/2022-06-23-stack/assets/consistent_hashing_1.svg create mode 100644 doc/talks/2022-06-23-stack/assets/consistent_hashing_2.svg create mode 100644 doc/talks/2022-06-23-stack/assets/consistent_hashing_3.svg create mode 100644 doc/talks/2022-06-23-stack/assets/consistent_hashing_4.svg create mode 100644 doc/talks/2022-06-23-stack/assets/deuxfleurs.svg create mode 100644 doc/talks/2022-06-23-stack/assets/endpoint-latency-dc.png create mode 100644 doc/talks/2022-06-23-stack/assets/garage.drawio.pdf create mode 100644 doc/talks/2022-06-23-stack/assets/garage.drawio.png create mode 100644 doc/talks/2022-06-23-stack/assets/garage2.drawio.png create mode 100644 doc/talks/2022-06-23-stack/assets/garage2a.drawio.pdf create mode 100644 doc/talks/2022-06-23-stack/assets/garage2b.drawio.pdf create mode 100644 doc/talks/2022-06-23-stack/assets/garage_tables.svg create mode 100644 doc/talks/2022-06-23-stack/assets/garageuses.png create mode 100644 doc/talks/2022-06-23-stack/assets/inframap.jpg create mode 100644 doc/talks/2022-06-23-stack/assets/location-aware.png create mode 100644 doc/talks/2022-06-23-stack/assets/logo_chatons.png create mode 100644 doc/talks/2022-06-23-stack/assets/map.png create mode 100644 doc/talks/2022-06-23-stack/assets/minio.png create mode 100644 doc/talks/2022-06-23-stack/assets/neptune.jpg create mode 100644 doc/talks/2022-06-23-stack/assets/quentin.jpg create mode 100644 doc/talks/2022-06-23-stack/assets/rust_logo.png create mode 100644 doc/talks/2022-06-23-stack/assets/slide1.png create mode 100644 doc/talks/2022-06-23-stack/assets/slide2.png create mode 100644 doc/talks/2022-06-23-stack/assets/slide3.png create mode 100644 doc/talks/2022-06-23-stack/assets/slideB1.png create mode 100644 doc/talks/2022-06-23-stack/assets/slideB2.png create mode 100644 doc/talks/2022-06-23-stack/assets/slideB3.png create mode 100644 doc/talks/2022-06-23-stack/assets/slides.svg create mode 100644 doc/talks/2022-06-23-stack/assets/slidesB.svg create mode 100644 doc/talks/2022-06-23-stack/talk.pdf create mode 100644 doc/talks/2022-06-23-stack/talk.tex diff --git a/doc/talks/2022-06-23-stack/.gitignore b/doc/talks/2022-06-23-stack/.gitignore new file mode 100644 index 00000000..121caa92 --- /dev/null +++ b/doc/talks/2022-06-23-stack/.gitignore @@ -0,0 +1,14 @@ +* + +!assets + +!.gitignore +!*.svg +!*.png +!*.jpg +!*.tex +!Makefile +!.gitignore +!assets/*.drawio.pdf + +!talk.pdf diff --git a/doc/talks/2022-06-23-stack/Makefile b/doc/talks/2022-06-23-stack/Makefile new file mode 100644 index 00000000..3f0f126f --- /dev/null +++ b/doc/talks/2022-06-23-stack/Makefile @@ -0,0 +1,5 @@ +talk.pdf: talk.tex assets/consistent_hashing_1.pdf assets/consistent_hashing_2.pdf assets/consistent_hashing_3.pdf assets/consistent_hashing_4.pdf assets/garage_tables.pdf assets/deuxfleurs.pdf + pdflatex talk.tex + +assets/%.pdf: assets/%.svg + inkscape -D -z --file=$^ --export-pdf=$@ diff --git a/doc/talks/2022-06-23-stack/assets/AGPLv3_Logo.png b/doc/talks/2022-06-23-stack/assets/AGPLv3_Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..445284a37ab3032fcfb1160420070df503d913fd GIT binary patch literal 32497 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 literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/aerogramme.png b/doc/talks/2022-06-23-stack/assets/aerogramme.png new file mode 100644 index 0000000000000000000000000000000000000000..3aabe3ad0a7726b260dbc7b233ce13a295d8215b GIT binary patch literal 117865 zcmeFYi8qx0`v*J{qEMemMO4NTWeK4ONer?x*%d~}zD(93p(HZ)T^Y-at?Z03;#0C0 zhAd;tHkPp)X3WepK7F6(oacY|-RI1?-{(Fv@B7-`*Y&!Vd23{#&B<|@0{{SUf^;65 z001nE<%yrCSQ)p0FuVoh#_p|S=?4ICbsT?~I=xDr7!NP`Yd!Hd^@8|ApZYojpirok ztEao4<5O>EDKB4_Y@*6#0N^SB^iad>WzHHtD96S*lSJPLKs7fHjorO=nw?$m)Q8V+ ztA(HXu}4L`D!p0R*xUOj{_cIOu}gq|p38UV@0U8Km867!6ukNLMlxYSoTK#W)zWJp zqSx5R?qZcWRN3e*oF|{OYHpE>?zg~Q6P>mpg=L~oGiYR7>W&=6c@8gpdj)b;Kbi0XFTBS-_>)6lM#Pc>deA`lYf_&FEU;IyZYI73c&Q=GJ^FH z;MCuT@-ipX|E~UTONI&kpDh2+hyee0dH{gSPXbY1Gv2l2oq=-k;Mt?nv$iv;wjRc< zONAti3$Ch|-1^sTL|C1S5ZRJWj&G)fZq%qPbNKIc`LnR4KXDk! z*F>AcA7;~Xe}8ZE0HE}y2m9GI={8w?<`=)}cComx7CNw7u`;*#+150SobN>$I<0oN z>%&Le>7YCG4N0F4tb{c?_m96Tk}M&Q{Lcg4_9G!%V>RPI_2i!Hshfw|s=If%Z3Ua! zAM^8gjEQHq<_Aro8hB5K(a$L;*jFZS)u>Jcw;y(f|CH-{cU?N~EN@tS`{zF4z67VC zPd}IwKXE1U1kanyvt3Wzf2g${uQLAeQ$6}quCcKpFXfX0zZVAKg~GD^?iH}$XChRVpe)GJR+KrR6T9D4bYg28XrOR__h~ZE7GPII<|17l?#=a6iLdAuTCfhf;{V{`q|8A!g zdKRD?JO;jF#}nk8h)~99*dU(M!(A6|Y9&1~UI&W-1kUn2iW0Qupv|3?fooN^{aL?y z2KJPg|2#7ty)X=2a?U>ET(G)k2G-vC@b5+f7xGM*W(Z&yBs!C-^`U1ML$dthP!hvd~h^l-VEZnMYl6W{o5!y6a=OU5)#I7mKd zI=adI5oJny-{3UZ3;icXH5uyscPIU#F9Xu5|KRxlyUeX5|7ub@*(uBv8@ zp}iTkr{PI=*tlU%cyn{+*`Kv_l z47Wy(w$#zdpmGH*y3{DIRGIuw8Nv?+B66mcpEILqv0z}Hnu!DBw|r}6EHeAHvQ9Ky>g*jol1t}Qu7Enc;p0@hr4%#( z-}quwYRN34uZr&J=*l78o6$_DuQpkz7SXLg!`J%}6R|j4GzYUn~Z;`9)TrY%v>nrZkPj$s^)#%-3u?p>% zbyI#8jDI8$Wk7mdu@2tU`H1-WN1{!f1NWl%&guD&?l7?3)7xQ6)3!!tIc-I|-(@fj zZuH=dY$x65NKdyhEWHd%>F%))kZCxE_y9nJX2(-Yrk8`9P^^K*;;P`uAzjlj-V_M{ za}x{Pc{p$`^s!_tEKh$H_Zb&1*c2Bx%=d6b5v-BKA&8vOsykWSq;b^`?BZqo^kRO^ zZ~L5#O|KS7Dm_NvW)b15m92dVvWTQIc)`{{aJmTN+$itpZT?27b5 zkc&O|i?jfiTiN)dy-{%K-Ceyr-hVq;$JE1i7HtB^lEh~Bqx(GnF$pMcf6B{vAHjVi z{^kK+Lwss>D571BWy|m9B!Zq*44Zj^E-zN4wH%Sn@gY*vv$_H6BT6j+3|`xyl}MXtc7P4Ajzil=NN(oclq6TJD=8LU$}mNB-&U z_9~HvARRwFD=ocOWqGe2w)tfPL!=5H>infdsaD_u<4|W4Tjfo>tE_(tRBeE*mImf^ z^OC!pGPV1EellH3&whF46u^{Iz5NI767MkI1)iXms(F*;YT%K(c>zl%B(u{r<{st2 z9s)&YMaMLCzK?J>ZP8TOedjm4{Z&F>IhFTaR!zv#V{X_|0Q5{C^nb6Gzp~9ZETb@L zP~MmPW@kvcCe{~l@!zlZw^MO)gFY@&7DKhg48iM0+QKfn*?%-l(ysndByMu!D7ll# zt|mL-MV^#MCvRNRegH3eY_BcTJU zd+OrD)T>6fd9$;47JawILjoR$ErYELfA;b$zLQwnNCnmf)-ELy#_p`5r>R3U*(0U@ zgtD{*}k z6&P*Yv@QLo_^_ge_s5*$!b%T1PK)ye5Pr8oq!SDV1aIHvV$D7sWNDZl*LSPCq4tN| zCC8(nrw3|!L6LohKdBF ziX>v{u7oAKu!#1g2x|E1pWR4iM>w_YwiZo+zaxtEaRWtRUz0lXNOJ@DyJJ|vzZNBP zCw87^CRGvd2N4fxyY(aJ1Lzg@!17YE3*1}68U=_i1ElBrBVM(2yYJvC2cQLhRi8eK zi@4J=f$u&!QPsMxkH)8Ex^>m*7J0TM4gWGVvWLn4NSPaypDQgUSKZ!IROM_myCiOy z*t9)VQdZ@WpmN^Kd_Pr^YLXh3s*CM@8Bk1u2NfIr zZ0}o@YNeNWVZ9!N|J60XVN$@?G$Nlwo+ydUIuvQR3O3&Z+igfA$$D&&nJ=%XC!>lN ziaW3mTJ0s~Q{UQb%Vh3bTV+dgowW9?aC5EXqH{zu!V50jWGzYZ;@9D66Yx{5R8is7 z8y%Ht30!sl0Z133&4SCo<)CGk)c3~G;R>cvczwY~Tck|0E5QV^!X=;I!-xdogrnBX z;?%4w%nU)Q7s}S;AXwtk-)^K06N%+-xn@55{a}eujlKj!URC;iZn;wy#Di7A z*thtwr+Bw^le0>{ho^Nn)BP{*>(=Gt%;>54r&;_^>^AR7p zNRryJEU`v3ANoWv`Osot@?cnhCGTFz;To>t=wb8W96L?qSfn$`?Y#-`U1^05*fNpQ z>0N0mLiOm|tin?}GzHG<1ScWDpynroTXEoAX&*UjNx~AG)`N%L=~d0SMv!)oq4(k& z-mjUS>FcFE9MnR7yh5b)Zz+*vMyW2}rVYEqCaLv4N*gXBeS#3h^kFHa=^z&fo3xvO z-njHFfH=II_K+e8g$46am}7G^-8eS86vNhEf2uZYWa--)o{m>}Q$m-hTeK%MH8p7a z-SHgS#jID$m~DOUaUSwzW=8+SVZG2ii$(he(S}t*XJ0(NM|=4A9wbQl=-qX2>sb`0 z>`9)94KLskju)npi^SrFUSUv|KUW*>4bKevW%Qu_G&=Iu*T!2fFCoKJ0ur`YWlU(9 z%p8yfo|{?cHo$LqD!rs_?<&KN1Rj1|RIAy_Vhca04-q}$J5H4%Vq74SzzXE0``!44 z#BCKl5!I`%Ah!co5MMK;CSYF?G^$Gu0ZD56AlXDPzwr5&=kHsItf$D!DI6^kX@-T3 zk8B}nskM)NqMvnP8!2e+{mBj+sHZPJZFtjbk{a)$L^1%K?({CzBWEHefMH+pI~_J9 zr7I3lr6n{)`n$J%+qFL*+)kM6_F|hY{hU{-%&REruVEK-{OXB6KXv90*6thh<>5cA z+6AGCwY7pC>wtruIKsMj7kDeC#hX@39ZD*50`jLm&z0PDp5@XCqe}D*-nue`=O3vM zEq@KO6KmrK9`RR_r86(v1Upg43NGtI-Fy4G$l@z$W~yoYK_>$)2O*|NPUHRyA2k{Z z7Z1`khm*j=*72P?PjfD@)^R5*I^z^zQ|ZMA>72(-Cna-C07OFFJ({@Yx_5PUS{H=< zuxtKa>L?kpBcl2YB3(%;Mw4`!(U5}}V>?F~X^VgZF_8Vm5llfy^*%xA{q7;V9rtO=6!y1ifahzGSuO7-b` z8o8UE>%@1iMKHgSR4HuDFi-gX|S+hFF^_q(@$0uOj_bp44f~ zU53J(1p4pgC?@(IIr!_1=Lh@0pUE&4!Mi}Bq`(VAJ&{}KyN^H(!($5+_|!GRA$+l! za=sAlG4s<;=VU~>9XYwYDXh0|KCX;rqtQe*1gZ1BL>%(x8T&yKtH;z~yV^^uRkVGP zEZIU!xIcN$HK1&dC#L(tySV0NXJ2aHG{!Q-if^@t8@iH~AL#bI*XXeyAMhI#v}bRh z@;VZ?O5?0w%>QI|_KEqGCsZTUpfvs@A`m&Wl=NH%_DM&Q}=@;)t|^ ztFfw*NaeaKy5trFFEKn0jm6{DmWlbkByKe2Jv?Eg?dS6tL?XBEdm(p+Z(|zfOj%>{ zpwZzoqpa`$NFmGbsVby!&3oz;t0a~*G*c#r9gES~h&v!@BY_#bFw)4+<{iQ-YwTB4 z?r;34*y@GWiv9`uC|+%rnAjAebYF-i!z%V6pmF_2Se3=uo9TmHaPwvwL_e}=(PQe< z3wyiAk1tX8>}^o)RTVV0wB*E`G@c>c>xid3YM*rE%C%uf^@{FP@*W()jQJ>t_bGpM z$zy>Uvh$H+VMQ%9N}a!t5v)@4wL#0^p;J!(nnnpblPL;IirD0?P66`!(tmyyq8tp@ zG1XJ!9Bti+CA)WXre3HHWrZuTVEvEfRWSbtZkIXEC4!$Jk~jk+&@jWnX<#-s$-fwl z?ZS_8`RJq3Sj)i@x2fjgUOOWfy3t^+LWd$3P4>~eu6dQ@GMuRN!!O}sbN7G<)z2-Y z9zSNo1_{;v3>Zeq+OM7sX4ASK#~I?-F&LYrQ* zMW?K`2T-{ST0EV8WO331!HjyQd)geGB509T>B4!)SZ%urDb2z1@w7)`KV=38Gkb%L`= zJe?tEza)$2A2I+>STtO8J(C8Mi8jO}!qC~ z%2YsP9=$~?U_e2YPdwRe&bWVIl1Izr=OF67*RLBTQbSerurK#_BIWjktf#AP{n8DZ zU+IZ;+mI4JQ&vK4M!%9VGlgNZ$RWeS(h_-=fq)dc#dN>?HP za0jHQM}7xt%nznn`_z0;Mt+e4;_oXk1a`Mr9$@3sG1~8YT58+(lbIF85X4d-@92fT z8SnpfOwWHG6jB$sZgtDH*-ZONfEnH|X149@fNk2>d&^gNpeuzTKAiGn+6nGg_S zzD8P+z$WQ)8esZ&_uTh4@K4 z5L55F8A5D@GUStraNmkXeyo6pC$-S35iqj_Kg`!&+IRfB^FfAtjUm5!^HTi7-7<<$ zfX6{B1=vdXRhET4y6zB6?y2#uTzu9D9EY9`tn5Ya;y1#<+$x(9m91nq1x4;N^V{Dd z-AO_7CdFE5vI-4-#vd;tOQg;0@0;+Lv&C54L=|B)Y2QF?Fsp72Lw7 z^gC~6A|2Cuhgb9xQh~hhl14m!mxzrLe`P74_l8Z;bpd|pPmJv6DTNC){_X8f?cBdX!Qafc z2G1UbmcHlTccnI~TG59+JzlysamuGjqB*3#@}R!XjK+&C4KgFOxzu^I?4?(TDrf&85mmzfN(z}3`k=`jHF-*8+s!YV$Wa3KJOhI3777)`4IjFxo!BL3!~abfN(W7ociis&{8+do zPYBy+RA0)wAN2T@Z&SR1m8`v3MTWF%5u)*J*S-f^?OtZscxzC8;0xE$%8K=IbeSdv z3PrlZ8Xzwwi)}(;$%}oJT-3qTaB8IkEWbS|^y1O~I}0FeT~$I!a}>@B(*8u>|?(qqJuSc(hN>ItPq9`H2Imul1Y!;xXC%^{j4_xGI) zsRXR3rC-5Xf@idcvX)#{_l66aV*%L;F)6;7+R@U?RA8ofAh}W2?VkUu{F%EaK`4is zluQg;R&^IU>N&;=n>h}UTXD?w{>$~YgKp4iMljnQ$y#T_hwT`#HBtoSD0y>674xj` zuL9(27U2EJOL_4GY+aw)eS35i8k#XSDuPcr<1B*F7>pI&A7O2Lh~rcT62y{2x!bPY z$cTHF=lu=SC%DX8H2lMPfunwKciP4)KdU;@ZfkIXIl=z3L!^Q70?*$5{z^Lbs0c{- zQgOw~|KX93h2SVcQYr_zGmuxrq6d-q;fx#3L7s#?1+K0c2a@jFetr={Jo@pCY(HA1 z1Z>A9{ZMdGfc?6O+0W7HUeM1&s(gxlm4poO1l~s zT)y~Z;r4d1Xtaimxo!UOQIo$YcbW(4ivcQ3N->fa7=(l9l8JzhoPl}2jvb8&1ZRzv zpA&#O_U&_l)C6Z0I{)oW)3M6Eyka`JTbM^jjkS)I%jdtC%d|8Y)OL>>52l| zh7alM$S@@d;elK~53I;4prD>Xc6b$|l?xXuU|I9UGHut~bx*YEG&ia&C{lG$^xNM) z%?}0**vl^~-_DG;sIU@|8;at-U;2p`h?F>3?Pc=?5i7&8xk_O@cDeEmAR%h9od0Mn zm>{ym8BXi?_AW8cn3r6n*`^P*yYR2Qdwq?4o~*(Kv9fZUzn1KE@?$=3dOG{|cG~0- zZYlHyVe>zQ0%CUg;50=4fic`Ovsa?#UwV?ir5oCIZ=I8ea{Sd*7;T7Yz z&Fsxu9@%xh>wWPNy>Z!zJ|5xy9<6q}+dW~#8J?i$;g!7G@id|7F>ZPw0J6^oRg?K1 z^0Fk;Ho1ih*q0`6JD3Dd_%);T7@O?h2Q<@3$jZbR$GXS_VbS)taf*>&vUdTS!{F&u z(}JBp6~H4sXcZ?y{~}$n`2}I^=>X|yNzLB6{>86InYa#Y3sO=jHsWyV)xJvgV;CLf z9NaZ_I#zXbc6s^IV=E=$vfQwz=E5J=B3o{2NymnbO}(-T+%pTHr#>3_T^J5{&cUG- zcKGicNh(8UYng^%)`0=w$DA)Fp3#ZoLj>VISg`wwAWZch}c@FnF-=B2G5Nm(Lc zUA@JI!{7SWd_mCCVAU2w5V_||jx_rqvHX(Yi_4o5Ai_ebN>e(!BSc%LxfpVAU(Wi@ zq~;*WJt6zEYugQDUpyb0!Wrv*z^0!G;s!7~_Zy*1ZqaPfx7UaJoQx65HCfuB{lW=K z540B~hG3+C#31$8M4S>3@+AJ3#jbd2)>sT#<`*Jzj~v*dz> z4*B1QHj}+l@LdH^)9~gxoB?X==%we76?6ASdsldkhgqYz8!6wT8i_hB7CxV|_HDn} z2E>LOnfLj)WRC58e<_5A?~kg~>sUz=*x>goUP|oA5?jadF!}GR08Xz#X7xn>XE)aU zkk!v^+*XjlI{C!P=G|M;s*kM$=W)MWvZkjhVWIf9-6uI~e@~w^@apMePG<8trR1o$ zbhS@amp4=xxz7(g&=azbpZ{j0q3zY==>2m{tU+BcgXd&iab1fi3M+K%RAcK%%^|Ij zHC4D^<)x71OwKWV$J*XC0noIGN^0G_CrDA~1=R95(^MezAVt^YYI0Zu)$-y9ShlkI z%pW=!9DZp;B_WW+OQe}V>B3ODG<5oICG@m>L05i<$Je(3tf`eJhA+NCUp?jD=OedE zpwzFx>q~Icuof#Ts~EM6GF@BY@$0(9-UX^#`hSLmyBdahnS~WvnoQh@^O3vAIKPEl+2M)n3&AmYXmz` zjY)^v2meAeANT&z+j~H2m>UHbhdz_uqGQs0527(>`^SI1xp&P5RPuhu)!rSl5kPA7 zxxX>{JQ?9-u=n_kV;!qS#GH)cA(O)l67bW_?)wc>o|7vBiLI6c&Ly`-a^|E|$}E!Gt2VFm!BGTD+^L!#IBv#B=3M$PAmxE;j8 z?TAFGqpH^x`;=V`bDD2xR%UJnhm8B^EG3Uo%wFNyOOgZU>ky1 zdbxC*W~Z&KFF44?Gp;&Q>lm{|u+I?fWovqX)TeF-0^iI7Hu!s#llN9yt5!BQC~&)3 zBE}Ij8ax?Rn5imGcs0B!DMBsGbOvHt7C%VGFYURDyf>??x)dIZraPQrr1J=OQ%$c6 zOjl)`MT0ooXoZ+&cM~ntC2!gEp{aelh0}#qC(gW5=d%y}!ie4BhduVFs*NV&r!6Y( zS#@=OyH~>4JdTo7#zUMzr5RK9>tS6QqR@0+O|1!Za@{HBptHc-2*05UGrLpAI{C9P z093V;+h2n05B|#g(QCk9Gd(YmV*4!>_UAIKXgDUgNYR0f-Y&C`0A%n#nYJYE?5?w2p4>^v|uYzmx0p{nErK7taqi?xPnUX1q!_r9YH6h}V(K6Sb0tGtHihUUR73ifB>OZ&yo(UzK^$ zKfbU;?U6O5tRK_*+zccG%|-MNdl$Tv*h~^uP0XzLusaQ3EQ;Ak-R)+#Llb-8gO3gD z93ZEG7z6~17L$2>6X`+oS=ErgcbJFhL$0e2@^`+J0MQ=2ox__>%jRh=l~gQ6<;Wh>6bNR-jT#1>|7S$xSW-bO*^)pmiTpy%4p=o5*LK zT?e85W>%LQ(&Dp46c>w9SHM^k7nh|?wkwcH+kyL(^5c`LO3T)c$ymJiaMNXP->~>_ ze|{ke5&EPKyRnjFox_yIcjjirr6+YB+ig5`PW>6pUhE8_+x#NY)5yrkAve&5;7TL# zfcyp3f%THCJmMdS5z<6*k}UM@O4eaCebkkcrcf)$U^RadCtFazHLytXRQhuVXW`0aJA#E-vOdu5rIq=Gjfl8zJr4+!R*ha7F_Dw@LBkAG#1Rr z2<|NERoXO}9789DN$gRk>2@VKzTFuE3aQ(NE%lywRKV(pYiGh0<|(h-k(;ZPLsYQB zg!#r;o59y27pNA6qQ_tT4A|pr_Ioflg(+BM+0^F!#U&Vs%|S1`Qp!?PN4ns@IrsDJ zcy!;I0zCW%a+j|w2XU`=O&_`;zMfrP1GYW{kJI0=jrD9A;=@M8iRjT@V%lzuQ)v+- zojsQ(NsusC6c`gDThZ?q|Xz9?8#HJ-dRq}G7Rm;mSi@lk0 zM;fsJ=xBfLy}EE}@NsJN?b+UjYF}TURkJ@bL3RHLwX#NQ8Vj`zcq4{mC&3l>N!^PE z{PKYKK$4!Z$h=(gCrsFO-hT-eW%|zw0A>L1nY25YH5-*jvz{YTA^R+#4Si0mx#>lKE?Es8gG~(@xlq}M7G6pbIpZ+Ba`)_Z7i_k*`q{^DqGm>LQnc?y3c;yD3aXQK06Aijku=o#^T^KavmI)it21>Nwe|0W29$Vz>S48 zSya3jiiNe9TZeE+qkAPnyL$s_^w4$2P=le$*6XyJmY;%lR?Nu;c`|3} z96Pxx?6G#)ie45x=2^m-nF%V}I=AdwZ8`<~;(ePYKP;`>ib>^YJ2<&lLTZ)_t{0m^ zo-IPLChsu|gP&T1t91%Rc%v%k@}|0n?U2EoZ72o0$u)=SM7FP`^q-7jI`2114+eXH zc-8JdQ&*9$M_z2tUmEIVS<3}F+!XUANy&w@vR0OWvmkpfbKT#k%i?S1kR7o6@j$4T zP%PzAcPB9HdYvk<&v2630J)=`ioK<4g3--SHnS}qR-GcjGt|1N_hP46lxiPu^UQeX ztDJYyTd13xi$FFZx2F$MO9qA(N(4V{s5Ti+hofv07f(w(s(j&X^zJjNs7OmXboTUK zhY-z3jea-~u#gIt$j+UtQityo@nw6fqwXCsoGTkuG)!GZ#pBK^(3~`B`^ro?Hm@w3 zGBYOe8jTU!yZjD3)#U4+_dlF9P(E)E?fb+7D&jB}f#|F1ep;Zyex~T!EA^tq>xRKa zHyFX^YaCqLVvt{f4A^|%<)?8nGUEi~g3bb}y5rFxA&{<=R|QUO^LkLFAULU7h063x z_U)?#HTQqkC`KnLC|y~{2$-lr9d7Hpk)4Y%KCczc zJJ-9NJxeuYKcjzo%09kB1;Ej!o(2xuC2RWmayAPbQs&n}5tRLoZHmYB>1D z>s`2F2U}*?w~+uX%z97~NR7*eaH#Yr+!1o)|dh@x`Y0pjm)_T zCV;?oQ3b#Yu1_u$8#ub0hgoze;`Fz!o;UrfB%Af$Uo_oE%74K{^lD#j&9OHbT#PwJ zD0g1`F7TMBH-Eh4;o*V%(kWQ>C*hvKX+9~HWp~vHoZ9nEaIn;i)g2{0ZVAyrQ zzbEBtoZ+4+{!xfzUPBK|8wSPrW7UlE3lrux?Tz4}yws|rXNmI_zFm{aZna&U0KcXj zsn&yMVI=f4j)D3L$|{?6`}+RW=-cvWjkkuO?|4p@-m<#(jmV ztgWn65l`6cz2G42Nv?^fEZTJbY3kmij0Q-oBgVdj$8DGY zVaPdoW+@JRU&=_O2>KiFv$t3;IG{Qx7V(kKSMX)1g->UfsmK*Eq~;(5H0?vWBCOJ> z3*rM=i6U<)cXELeb;(+{MSMRyF6jopMVRtb!Jeelf(JK%cr_bdr`zkh{-7n0Rr$dV z31lTq&?dIJ0Q0XYkGjU)OO`S};_l`LhE3obL^lS~`~#~gbl{M3?A_@uxScrR31|rD z0pBe^)7=mxe{tEGX!z810;6PWvXDPZ(rx~Z7js*e2edtX&OoBj!yBVo*?nQr=_T~< zrmuFQyK+UEy=NcfhHiI}8C|84eEQK+@UM4v@{4{==)jGs3PzFg5x=6RnZ3RJAx2-c z@Y_4S^fIdGv%aq~zUzd(QFcJGccr4)Q)@$WM{W>D@wfMR+u6;$g5qhN6)lNnw{xLR zeQGLohRCOHI$_I<`7))+41}~USq@Z^HD*bqZOU}oh`Cy9lPy4H+M%*FIQab{r2>?> zOwEJ{#__X*7(0!o=@X%pVJC;~z)zd_t;vIgOePfqKK~r-TjPVED2F1@<&btXWQA^djZs}HMkJf zLjR0VL{t7m;fFr9_ge8P76oDEHpEP8^u!4Pb!v>{UKGO|(=?*jdOeR^_VVs$J!5M$%aO-@IfqigvupMer54wBBh*v3eC(+lu;1$U72QMQ> z&rtx8Tjl^pab1rqoL z4ILJMwIJAdz(*h--1G)-?6Vpk@a|)<&EcX#Co8F85jwyWX_@5 zJp%5xb9`fR=t`KU*$(=C!$tJLr^*^TT<~>--WOjG`J$sYEK3>-S3b?Fz%oMf@<;D! z_Z?d3lJgLS$sZZkE|S0H1Uk(N9>9A+z)cjxn0rf3ay1YTSUhiza{HTF$Jdqt{6=mH zKQlnULR1wH-wSgmN=us4QZGmP@AJgm^PhBP^ivqc0P;vfYteDnZf9v=bh1ek8adtI zm4C-;s+^PmPH9a|^ZA$7*4f9gfq^f|>gocndttgwLz=tsp+me8eVOWPc`Igz(_mBg z9Y)8bec$8&tTrt9cpz=XoP}%XNVq9MfBTsaZ>85tjI}5P3Dz@z&cvvvZf;nDs-Psg0Y(ix2-9tTGj>RVL zCheNp%k}6q>u>GO8x9MEL`%$08}fiZcvLiX@^*a*VLd!z6faMMuyFr(Xu4=X`Hjja zl=!)oc1B-C8y^PE-8m{@{G_l8x-~J}WRSe;1(!AD#02cAzxDK zJXL*ANRQlzxCHlWsqv5Z`go{ayxH$uA*ViUWS<(V z1q&BUA1NUt&NFH_?82Bc%up{Pq&6@Dz>;pWm5<+t;3>p`;lrSBt8@@j?HcE-X&l`~ zP`7yyFQkhl5GlQD!5~AOyT*goeQTP;R@u4<<*pqFl&-$c0^i#T|DfWQn;lG@8sC&w zrh&dOS=oYW-tNS{+_)om&v1+959HfW3N~wMpjXP+d3=KlROYVv019jGl>*yPp(75Y z?8&ZaEop116(Dj&ZwavccF=z-Qn^suW>7oPI2N(OY>pUjz?2*89 z;iX-|P(ZJ2XS_ArbkyGOe&$qw(V)K^l@89Utm7X~z4;xSmtbv|UDZzxV}78DG%%l$ z(QwFhydC`jC-jUlk-;Sg6|ZdW0R=~`oQwcPNmEqTP9r)msrl4T$_)N6&uxBy2D5p(6}!8H!qpaf%H>Zjr;_$G(8)kWL9&P%CI~c?1}@4i-oo++_yHlrpNvFv{Mj zYS4CF!-l_>wgi%vjNT70c+BF^g)%Rzg>Kj#%fp_wozB)WJvmukq?hh?xV>Oi``ozb zlV{xe%F5_s|CH}RgHkrG(=NO@;dvhcteW~=kgzC}=Q$|*AH6%g2Kuk5f_l7YJ!mIX@p(A>CO~$9D1Wa zY4@>Q$UC%}Tzkk%nLTn1Byi_sg>Zw&Uhtz_61d0?=A7Y2H8QDUwd5Y?H85&1C;+O? z6La_WlY!7_Pe_sHfJ=sDKVEECTCB?|wn|r!@3~Xzr@hy3&jJD)V21t$I=y9_YRTLQ zbAb$Mp1N9K#`5J;frBOgGwBHDin4dk9_}0K0b0ZOx{Au?3unPM%gZWxPp)1U|9#J0 zlb~hw;Ge-{?VhLgqbek`?hwAIYkoy=~}=2SkV*i$80$^MqRAwKxe z=QobNrDfCx8*{M2KYn#@dOJl_3&&*F0zN0@OR7IthR=lav+FDTzq0_WdY3O@yB-3C z8eShgw9%d$Ij~*J+6+!&5UPKZ-4Jl%&Bu z{6*PUhdWDNYcu?(fD78gK#rEfoy)9?&OUuiQ5Sy~(FQ!XtTxbAQI-)t|3nhsKT1TV zpE|RiP_6Pr0yR>%sLJoi5DBpBj010_`&#B}5rCG{X`5$kGdHTH>`bGjn=p{`2|e7? zMmXo|)zX!DGf9n$!eUHfZ+|GZVH%z6$}>GmnA8B^KXf>cAh;l6Qa#Os78Yjab((K4Jd!JFm^_WvJ`QK*qOKX_(z9mLL!nVPF@1?I<; zahEn`zRJGd@^12dI+@M=Lk7BI3sNK}~bI;Gf^Pwp-VI#VXIQFElRH655K|DzE!ype$mq+(~MlpLS^^ zqJ@I-FvWk83g6@c&Q$$MANtq&abk^hOXLq`)a9BeAM35VV)^V>uV*$cOx+8Cv4+ss z28u`N*31G8NWGf(vfuq*bV<7LooS6=Jr`gbVeCw+ouE&b(}Xu<^-`-6c1^*P_i_LBmlwHY3@q?#z@}=?gjCU1f8DQ8{(bV##aP~K zR9fjuU7O>SwWa*pb%E6<<~Y7ndgNJn#I=r6%7ue0)c252YU2F?sK}rxNNYpr9no_j z=DGKo+P3XGSF2<}y62Mm+k_igr5;yA`PZTPU!Y5m&silr4REx$Aa|eS>YewJUER#G zsXpHRbe7e2$G;}VwmocWLju^bUE zb{Tb}{D3YsuL}jNhrZz&CYs{{y*UjrgWcm9?00#k-^N}wuRCeM+svH(VR$0r)Gqw3 zY&x6d0f%1UNZPU6k9Vyy3%DYKo0t?*Jb!j3zfWcQpP5TuazZab59e-qXgv(vaqEu8 zLL(F&v#K_kJl}5g(AD9d$T^}q^uY;~*0h;_x-_NU;*3XDcZLhFC zM6h|g2rspxAwQz7`>MBmy*_CAaNNJ?^Bct5AGg{j)broq>CEBpeU`y(haMNR7)P_u z#^ij&xtI^S)m5`FxG*eF8M@JYrR*-2Ql3?tZ4qFtTP_c2#GT zPU+t3+%LD%RYcSWf8VTR7|mU>wRZpVEJ`(WC-V1Ltyujc1wo)OHof@K3zlcyJ+Fnr zj;GgxkwAhHrDG9+1V(k;gLu`T&eWkH-})AMIgQQW@CSBi6dMH6 z0(DTJlVd*ujZpiUHgkrfFJen!S3G=Y0wG>8D5||DW0ug0?|m9obPOk%GUXRsLb$CqFpO(Zzkm z-5~zgex8;|9oCZm1X9KHlM&N#S}RabQn7pSZI_EWHJiu*ml2$GeR=dM4v%iUwI7K# zjp`!Bj1ptyBY(NDw1U}Qm%hNbXSq(sX0piM7@_vh!t+^OOTD9X z({pCN50g%39iGm3!|L%0zaN<3(D|5v3%(z(J;I%rM9ZkcToF>e_vhb7%ZTuo46#v)fEak<> zlorf3mj2Wgv-lG}Pj5QgN-RnmZ#r}p=QBBwC*I>@IAj4+BF1|krJYFcxF8#l#4Q&7 z=|=i=8$o8o?Ja?i*q@G);b1tb)fD@Nt8^=(kV+zh-{Qx3$*NjeV>+wOMNKpGYG64_ zGVti}S{B*36R+evpRe@Xp7ze=I7ISB8PvsvE+T{D*o2De^s@#V*B!+NQ+MM6beC$R z_hv$OVmB;yhBNe_HXwb_63*qju4^|#z&}uh(j|&`)}Cl1rQ@EDQma%@{-kbL_fe|4 z59`hJcO&~r+11W-dj##0?7ENAr1?pUr`|DxSw^}xw^xCnZYw8JOg|G&ox@Hhs<8O*!K5mxZ9AhPRP+gU( z4#sFXbfc~0oJ2WSi4IaN=nIbh9tB5TL0dX_tJF(s z(s$3-e!nE9ok&$auXj89#{PqAOSBMeIwh<~dLW6+3pW{gQ*Fm{5kB|xx{66|qovK% zkJbR4!0(nDmVPv;)cf>dzf#CcZ%Nl34d0kbN>hk8fRfFnWFQ0cE^cnup(ZKMdIJOC zfO9b?Cz%|WJ&}z-`B~kTa2%DM9o5bq>(ztRT|a95-1$JxRx4{Sg5H%!Pwyfkd#@>_ z<3`L#?&6I}`pqszvmBzL(p3W^8g{nB6P>b^*MP-M-_lEgz`k;uQ4KGZFK@3><|?Z= z+Xjbkjqsoh{*H+yxW9Jm&i!DS^e!`A(2c)1-!+wkE$Ap@f0Z>oZPR~AL})fk%l5N; z(CnqQ>&yId;al#<7C}QF7}-5k#ksO~r&{QRXG3YfI^n?&tz*b0{g_H*DXV5BR&+)x z3~U>6Pk*G(C%v~~NPK{#e6oJQf+6?+Z5jXjLsVBBwyoXuJJC4~z%`fT*ye!zw`4G!Qw zP)_=Q=Ivr(Xm$9KQ4{x~=}ue1ZOi<>wa7Kpv=Oxxd)fo!X-m-}d;Cb^ zrh--W$5W(A>1yPH##2K6h}VSvHmPe&lu))hZ;-e!EpIBG}rPeQ+Vj|Ik`z zxH-avYE?~Kfoo3Dr}GtkSh|X^(3Q6$Yf9crn70{5%R@DCEEo#r>-LW*wn-2TXi*&# zx03tN5ZW^avm16i9%DV+FzOD4euCu-rC<1*w~`hTaXuJUx}NAB78v$x+VVU07A$Uf zlIu1!pKmj8A<@XU4U{!A`qGhZjMkqMqY2~mIOiv@;O7GGK$Ej+p}yMqZrZxJtpa70 z8ef_|Vh&7pu}# z;9`oTgQ+J$j6-i}XnWJYKf9pgpHyA)ixCapYA@w*TeNhOo*tb080LwsVs1wxmyvD7 ztF%$kB8;dmA*{q>crcY!A)1p_~@butwAavdPBH^U%Bq@^zvqH z30o;G@%p$Dq?Up808*22o*prvE_Dx%KOo!-wg&N18A)doN1GiExd|5&2X;Sy+PE$A z5WAvX#|$6!>A!5S{d^hLHU{C7RS?l&-Wbn>pMPV=H#60F*nW=>F4-9G4iw8U%x*xPzPoN3*shq)2E0ZUxvKX z3rmhTb%m}anq$G!OZ>24YbY79x+!v;;N(#Y<>R9)Ba98SKDBGw$O23u=U(!44oD5T zctm8uQx~KPaYKoV`%B{a9AlZ0t#m-bhiclqpx0~{&-KMiSG^f84_EiNY(uF^Q)&ge zRVJxKZU45YW+a46_&J?^Xc_a+g#Z41UhMKzU%$Trn4%BdlyUp(J0@qIsVBKzXgU3) ze^T9pOPJzOt-2GsevAFEm5-U*{a9WyrOelu(#WN8$>_8F40CS?81~z=jLUu&oxaug zc|BLAIa`Jg@)8*nw&(l^Be&pc)t4Sx;_|zRt>Ffp97V%A%iNEny=Y4nismx(hUgej z!gVkY(nIFRE66-Ze{=bYed{~P%-qHjvL3HyH;}F>KJrozB7eYgRIE$UrHe0RfvY?s zd3i6d4Q0Ok8poghikNMy`=A@`}J`&1qICULW(g2HKN@ag`*XfS@t_eW1Z z759FmrID5vPiKyN#46f?d%aZ}SM&rQ_RJ#J=3F`SvA{}<>Fk7|zQ27!%~S(V8Vo;G zXp^lfxdfFFq0X~KY-uG-^ci{6)2aYuYN~b;wkJn*bUPuP@1@W&* zVJ-FRnYEqo+sW!09B`jlR1gC*Gcy>G>9+Zv>=WY&X}@`T8(UA7m6#Ent?e9HB+rpc zz=E+{vEP(dyX%F(kxozdneT1oa(0!y^Io{~AgeYJ-58N9*Y=Z}H+*Q!sTGMPffuhE zOcsa6Shn1!i_m5r%&DYjpnb}~Q#IIBQJmTxK!8{+K{`3QG|=;?(YhOVWz+RE3Dzsr zULjb|o}cd3&NPV* zJ4Yk{%L`071F|<$sTA2XiF;~lYumO)oK&Z)X4S|X9UQ-*8B!%hTfGGLS}kFfrnY*O z9wyX(qfAETY%O)iqltnFrWd5)9WxzNxdR5rRbeE9gQ(%Nb~B56hYge;8kd@~6lXRE zi)%-yFyk3^hrgF8ko3=@IZn+wOZWlsH?Kk6$ui)YzWE}mPvWnNwjf<4o|(I+b6ZsYd(n`XfMZU!^wz=zM>1ad%ccjkkef?9`SR`)ndzl_@< zHba9gY_5LOzj%@P)SR(SsdZRZhuo2Koc2IPtjY|!JJ50?*yG;uMA0i-JTMs9vM@H5 ziazPa<~29>tL(2e>ES;#ZPfS#1X%R&5Vu`=QinFL4>ih~P&nor4>nO=e0@k1!MaNS za<=Zgc`m5MzS(w9&oo6DMN~mpko%BvpD1UTx)~@&1{xXQH zS8Iha{k!akfR;uFj)Qm_N#A=zmIe=+F4Sn1{)k+=!WO4P#sMaQIs_N#zfd{cm}*=0 zXph`CFfd4#X*}24Et;5l3cR3`tKEWx$m1hN$fyiR$h;*>M&0dR;*GA9iSX`qKD|a2 zzFlNqEBd*me3J|IB2HWbJOV!LV7@_8NZe4m+8Gt?Zj9;*!MaT@3{)a#s~d+Rc&-W| zUwT?6f`MKg(5*H-ru zu2fj-ZJ2S=Q3d|Jk$y&OxLA-QHMI7+Rx?4g0gJ#f|+mt;s)@)+& zkGipwllm$L8-s1jo9&T@I;N6{m0d{3dpzWe-QWH@WuD{*$;dxLWc!hM${)NM!bobR zZgNu%rj+pp?+9+)T{<}<<#Oz2YZ!a})tyEx%9kB+=i1uLF&j?bE~Bt3)D1F6tW0#c zJuPntbNbHQM7s^%ytr6x37MLh>P%@Npp2vUZO8=9@mziQt|?9ecNHe0<8Mb0Tj-fA zAYV&DfOzF(&S7NqmTa94)5E=wuBtahj+br7drPH$2Z-6Il;3+9>0U#lWcbHV?j~Eo zhoV5V6ccPK6t#-s)mzTF>p6D=y91FNc8$EH`q;)}dY~l`za|$nFm+m3KuVuN)x0z8 zm$vsSi7cwMu%E1Kyh3(JX|LUdDEzHi;?AJtnR>y89M601s=hgUo9dnn1?*8Mstyrq z<%qai9oD0(7}_*?*Fx(&RM%%L6uu`1Q{w5k%egHf&Su7ixP}l#$X3wUg`l~m0pgby zuL|`grrDDLI_s6mECWkWXvf^@`J%?U1$98oL2T+N#$~cVZS~GVpiOsiVg?z_u zF_VEDCazW8`?6KVAH$fAvRchFaCWQNgV)s@H=2L21S4MSu@Nq4LBkL`q>^ zb`M~289x{ky%7dYV9|#|uhkcFu;y-0$nEH~@D)$niV3ON86tVpZ0qnTELdi2=M_UD zrKgr4au4Q{+*X@$0(GFYqetkzoq~g$N>(eQ;{GWictaF?McixQNcFMC2n7j|lbM+9 z{euKsh7eA=sgUK%hsVQ*df6r1UP-nG$2aUj1BIm7>hX;3P>})A{K$ z!{U2g?zJ;bM@y!ZK~jssiOr*@gCZSOLs%~CIeOeK(uK9dmlpA^LCyvSkCG6jP351Y zH`{VV5rz$Xn1Xh_P0;$eq76s`bP2AAdiR_FP)Xf^b}$kLqB>Pun05MO@#-U@DxavX z+gOKOzi$;@I5HgAa>I@g5Whqjkhh!IL$)^Vg)YU_m8R+};%(KcWF(JY6Wt4cAYA6J zAkcS}S3hgi02==>L=y2d#AalXxSnMcJ~HLIIeVggXGTgasQnwC3T@^NQzA*M6ssYz zvfyhf)$Z_W`fpsNVTf)OxH(4GO1B^_b=E6rOSdqqV0t62?;ew1|1{!}R zkGq@r80|vP1N6vpHK=5mCDIjS`)zXoIS4!Ggmw}t%1;Hp*&ve91FS;AenC+io3L0P z|K*bLCv=U?F0Ja%k&p_1q47@_uL$p;osz%&V(;0F4F(4(&qViUHuAhjjP+5_d_p?aL zzVpyXdA2=rhxa-IPoEE=`^<(1ZK+f{_VC519zfkJE4p=FO0WhrM*{SA@IQ zdi-%)=*wj#wK8bm^!iu5WTHcUUmeMeUVF&4NsL22(y@R#WccawR3)QDx2IL28)>lT zCO*!w^XHiU&JQfFY+rPq=QISDqo=3JG6}3uUR(zxlY?*hYh`6zzmA&H39Ck**yPO< z{zw0`RudRg-g^1kDzvT5vLVLRwDQfaZR=~hD&Rs+D&44gOb5o7dJc~$1eapdLwhkg= zv-XxqOEdkPcy9n@G=?ViSE6(@hT;z{)^8Z6{Dqkl>xO~e3r9%+D7v{M!zeP8yk$J|ek?Bjwcg>^a~8D6&vp4)@f z-Kr*x9VDXa#<)RhzN|sNIO^n%D^G&L%6mdCIHh^M=SJUGk5j>H{I2nv_|$KqLs-XG zC|!-hc4G=MkAhmCny1S}0H&+JS1Cn4Cr#U(Q{)KV-D@OX6B~2iaZ?S07xefYt&c4i z=+3V7C8E~DOI5tDzPVvPYKAEqGB&9*r*?sI6Jw|_6n{yANZ2aS-yhrmM#2%HTc$)4 zC99ED=LR4rBF>Ukc%GHL6OPd;PvWoj8V&9Rjs!lMmco075QFvHkkh#yvp>*Gx+Rg@ z8>D5XL-1yN5EhILQ8ln)h>E~YpijIPw&EZ*k!Yv7q4b@6mt)oONcGP|X51P`$RxSo9b zI7lb8ynnc1$@GvCtuegHbru2TzF7U1NnMQEeJow~Q6*$K&!;>WHR>H6>;Fj6-*gRN=9rDxb;CXdRTc(M$gpusl6iN{HB zrha29M9*|$I`py2HqEYx8KH-Tt(7Q;Sq`Yp&8PTIj0UHk{#dfli~rpP=qD6r8&i&% zAgZyasWGB^1y5~Ft*a{F$LNBg3nY9kM`zm2Psf5!FXN6dGu8V~PpIu@j_x5c!6lg%UEhM@!Cqo?!8OwZ8l5TbaaAxyVFh6 z5BfeE5a^n*`+cG+s=MqBul|s~o#Pk|(;QO3<+qaMS>$V0xo8yMwC z{U-EXshD*+E-C}GuN;~~pF5e+sPu`gptn@#MpA5|mCK7iT*$^AMSGT$z4J)0_w%ymB z+$<<@8IUlD-6lYn6z6hfDbc zBoQGC{}u}D3rrjPDU^2->fk#0kQd1XJD{dpAyFh{TWe7vKC3a@ z-Qh!fk&RE8OW~R@CJYDMJs9YbpgL>-jvn{jFwRn}#P90np77f%Y~|>|ysRxr+}5-s z-R9n9fnm+h%0F< zBdkTFUz)dgl{C?l`z79P_n}qa+&*e522p#l#4RrhoXyMlJdMOIumS(K`+-376^G%q zN!IryuQ-kBuQ!X8(rIDuQr|aacw6l~qugN4t0C)TW8G@(Nv-A^Em1~q=Cn0{KCzdo zl;XhL@V^J^)EEoBbM2&{fb&gQDV`}q9l?aKFV^^A=63pEG6`3QA+dSPFgJSBpN0A6 zp|8lBQnZZF5Wv(kC%%#75^=$NOk=$xRH!nG(aR6k2S6!CW)HR=!GfhbK39C_0ju5Q z2G&eK+w7a(C*+g#(~=NW*Cu8xSQ+OEW4~Fg!r5>gqvCPn(guA}&;Xa6q#wUC>2zN8 zEbcqK9`t0ELpL5(bLx!yx;@?j1g`Fbc^zL+p=Thw)G?ItMY^FLGo4H!=({`d46+&d zb8Lg*{^czwtCxQ;(8haS+)%cCt6>B#>Aa|dNiBB+3K_Ti?hYMA5Aiy;!NgK zh+b{QUI25w#6iDcEI-bk?pPJ70iTn;@m*k={iJ}0!Jqx_RbkRYv!T!k+I>gHSB@G% zah4a?Q&CnF&0(iJ#U#ver9LDczF~xtJGxM|CR9Mwh^(UDBfq8H=8|tZsu{QqllAj- z_2bIRiLTMguQpqBivhKsQfFV3P|b{Hp$Mj}mL2qoGuOy7V@T5SJBb$@_$ltm(6BG) z6Sp}o0zrCckh^o6m&I4X!|URG-Gr{uEI-MePwTR8NuKL8ltM8DaMMj|ogSHH)Xx@= zj2z3YQo(7@4^Oe-xs^$#fxjk8n1l_^veQ!@4$&fMp$<$|4OnC6qnX`StSe-fr=sHL zTNFz`rr=u;)zIKE=roUpwx)gEVxG+UFxrUbSp8+W)wVGqhu_5cTXj z#n3cqaX|v-Zw5nsZ7awwh$hkKFR#df5ne|=zYeG{ceYc1jEDVFXLZnBwtt8IUb{R# zx{D88w;X)uV7(H5Q~FpQ8b9`k)etdwc3(?%n?Cf5nBk?5_sre3?1jm6XFXu=MSNK3 zTQDht=0JglXg$&r?OlsfL<#+w%3Az15!v$oiWR-!bB$Dt38?%AyI5Hc;rru!!Nw7ZWCQ4eZ|XXO$vqk0)6ukZ%i=uQ z;O6Ds&^3(bA@_5skLVa=FLXf$_r!|tYu8h8VoxbB{959Ju(sEY2Qt5u0m|o$K0$l} zPm6JUD7e3f8H+Lk79X(Qe}kv@O(VZ&F1C2AQepYj^MyQyXJz#i0Y{5XU&;jw{oT> zCUC01!r(N>q5MVq?JuHd3{U;wS8YeNZNRqMl;w->CzCG3eq_C{qsG3sf3K+ZM&L?V zV|uvFhKejJf>imltD5O1*WP;-iE%fPS{9?BI>Me6?Kb)3!|YbN_JFyRJ*(T0|3gvlRS^Fd&olNI6BX0%KMNoh8eWqpim=|Oj;SQ%(ypr`xdl^noSVDkK z33t93!U9Vm(uq5SHq1sG4=CB~r;(JTywi=zD_$>EZ_>+5(DtZ5zj?$Pw7a`CL#^Ub zY5%?zlt>PfzE&WED-vuy)v2wqt@>u$Iq&ni$rj4dF+|=ReLCtxuy&r)>@_?IosQAcn@+tbf-bP& zo&mMGEM2=KqPu1g>L6cX=j$zhGNnHCJ9iC|I)l;-ofM;KQfKok8TrBOHRkb*%+5IF z@;&)F(D%A=bREO;H$3yhwgTO83;N>spG-oJkA6XDmEN)q?082qz;eaxxz5 zC@m{a$8tpx=Uze7->FnM&DoHLqoYeNMeAe)#_@hbh>NG6Q0ssMk!xY(e@-E8z-01W zv#ey!Z$W%p(-&2Nk4_@GUeLmKs9;Cl=}o?~wQFGtq`u_Z6g^DWvIIi1(=#pie*NI< zj~&l{d_*&mlYEKW9=^@t_@T1=uradt`!W_0bE6GSccC2xE{)OcVYsle{y8K zTJKvW$8mC{E=B|01}V5Vc+*>WwM?n9AGnE{1Cw2eX5i|udUYC0>Q8(ZEk_|)`K#j; zJ3Vb_e}9G&>Oi%P#$UMMa)&*xK!ASpB{=kJ-#V=MA*DUH zVCP!lt(9bCK`Kg@XHlMQcc_(DBKCh|poP>A9Dm+4aSCsSl{$-s?yL&L4a22)lTho? zmNkKb)lNjAQ95~g&g(HPD3o6Xr^!J~6}}SNM?-~kq&_3Xn8RKI&C~v`JVb#3udfp| zfc@rNNNkGG)F}+FvoAv?A37rHNG|ECFR83ra@nlt5g4>+yq_50O!D|u>yfB@;wI?yOH^!UUm-bRq@b}LN_2N{ z!fpF_f*w-C1&sMh$*#-RUXE84J_f#6W@sTXeZg!>U+ep$XracztSm#{0LE-dr=-LX z5qrl8;&k7y^hg0+lCksA&<#=L^K$-zFS-5wLaTd^iz!eNB8xtQO+hI|R0 zeV>aqaw^i0gFCdeQ3EV^9H*6tS^&#}Q^6~AO$kOrBTuaC`EagQUY@tBj6WSg9qwF{ z?`(rfTTtHyQWlw}HW5N>ay_sOKWt)0FV2YvxiVROj%o3%oo?9ckk$kREGt}*Jr(N< zT(g{%9EW>O1t}1pd+Nj|5Vsl+oqvs|6=A2rIr7lJhPEAfP?}CS)?CgChTYaa;oI~f z`rv-j&5yHwNw%IdT+`3GrGem5%d9tm3+~^$+N)t~J|(}13nI#{O-ao3lH3&_ZBkjH?WPBTXGIUn5;o-D$j-m@c&vJ8Ou@D4B+e@WxZuhfA??6EL1^x|$!^vc_vyAw>d1NwVqEY4S>%Ss-{t{_c( z;?UFKnRE!biVnm4_t?93)u_oWp5rIk@Cfw#S3=iYo$R|+w$hH#@;8rC@urNTFGi7O zq5N;pPT|c9IegF0pXb8us{x2v`I$1O9D3vXXj{ib8|Ycj!$0cNqt9~6LH<+EUlR=w z7tl3bzIMfWvU!+ZG)x~y&u4Av*^yg)-4>eM9YaWCmoY3yvvt0eyBD897$wu)fu@co z?9}ul6%X|u{YE#LSX=|#mTLn3{tC&*R z7XJBy`fOQNY-e{?X?#DxF?ozi)n1p_MlaVG!G7o)3sgqdOSsSNR^Z4sOyytW8k;Zg2Spa;VX!DRTaBA3>MX!Y{v{pAQLx)>xQ zI^)k63nVG3)X1NLnl(9?D60a!$IA8$U`zE(8cX!W&F|)Ry#-2s=(z}k+kW$`vC2Qq z8Zg?jlu29?P(#@i(=Tz^c zFFI&7Z_#xPSZH`G`NtpIJ*;V8=@JG%i*=^|uh(`hRysI{22q68cVCj;MMyD#F3@OM zjrAFNGf<21w+~z3P}3nlNXIgCCEz}uSa9BJ`{5Wl$ zwT_U#ssp-dpP>$|?_fMrChv-uSa-s4VqX_*y^=W(Mj603Z)F+Jxypx-Se7W;#zd>4NG zZCpW_pw!sxr)Z?XO42#?XLBa6{j$&9G)~@51iQzeH2rXpNw=mhvBTSKL=4E&IYud_ z#EG!WJ()c<`xYKG@8VK5|1@+fyXU&5So}**_5PRG`#3F}D9l~ENCPlH$0vHal~<)J zel~BXyghY7n7CyGfv|9z9(N+w4#z(<3}JpZR05l+tV5|Tm%$dEPJd0K<#<%Aw(WU|lA}e!q%%(Y;LHF0&f2b0I8BQyy^@ zp;Tob`}iHkBDkSs^$aynI%)}1*{}CTaw4?8Yui-I|GGTG9TW|_dbzCx!eP1U&HtR<7&!-t6bL~D< zt#qRb9aXo)*_kWRIp6ONlw)(6`xIaYC&TeTF9UE~KFGOeT@XHj&}>LRTV#v^OY?>7 zY1Mul?^>G#gjsb~93KK&BMH(D>8Eulvh7a8&uIGI-AE^7es_K7A#p`J4+&34l^lj- z<`2sL6ch@kA+2vHXaW2}ca<)Y!81;Im&>;t+z%k!KgY<&jNsXI&HGgN`6qH#Eo^Jr zwVYYkNE|17)8zbk6tWJx=#d?1uXbF{tVy%3l02b0CBkr~(c7?ULK{m~8|xE(6P{d; z2a@f15NHrnbLx)Mg;^&Oo}Bl4OY3FSTa&mVyY_f?bgQjvcQ^k>ona}GdQ7-c* z%94v&)s7abT;D!8x6o-+(M(;(clRi!8r3ezhNyNr5eVB zH>xId*?HwFe|73jbB2D~yqvum9F$EMYx6R;0QB|XonKAGK2=YtR1$l?V>jQMoEBui zd8*g``baXnKDq|*9o`ivH`5U@LYmr%?$vc-pn5X#c^ZD7&`{82rW*sN5v_An>x7Yq zxx(rsXTw?KJ8@47R<6U21xk0gh@6#qWDRvH z*9A`&Op{rQ-On7lxrwqRuXgqNr`fCAB$uI7$|9IqKn?mk%oxdvWPd_cO7foE!x)yi zL?|}6QK(o_4l*x@Z#fb0d#o!_3BWM(;g3J(1_k+ZzE^w6-h4ey@p%V!EMNa-Mk*K7 zrksrCUEn`mG?mDAWjWpFf82R?mHXXb#^;sA=lubRqUos!+>e898a~EHL_e96y`bM- zuQ3O4Rr$p}ChVuhphMmlj{*m$#cMztiIb5Mz|tXAmTO!iz#YZ|xroaunICvAMqD?0 z4up1f@mHobFaKa?W!s8gNO9i)?x3e)Zd>^~+i+9by2*PMW;Da7^tmEU$Yqhk>tx*& z(4CI_JO2v>2Ncre7~n@|d4&n}ej)L-L^V&Iy8-I!g5(~CL#GTJZvkhC_M=_V@?256 zo{|GKhXvHMM-|BP2UVdN#6(uVCQ+BOIu@#Qp~|(H2jNP=UTBMh$#OvxK5>;b?f8sy z7r^n!2-#VJHl4H!D-p#{#E33FjBcK}b<76PepF{9@Kh(@&U-}LJMqjBp{ zZmRxpsC-qMHdj8ahVt=<*>QUHC#k{IMPn<*vDy+Jik|Jwr?Hz2?{8c8j_+HDq@==h zZBB^U%k^^!Cf$fexe;5@WKlYNXFinLro|ZvK|KJ)y4|E3V>x1~a{X3uYjf}5CQNVE znFB~V8a8A2=RN2S<45wy>-q)4!rgWqHT^%V(ns>gL(1J%i?rka8h`65kJiE$ZPN-W zlt(wOkF0PnBaSA+9bOL(|4T`!gm*}8BLsc|>B2UQ0vn9Bc>A9@4>h4A_|E}+Z|JH6 zDYgS)+5d${y38=XwDIlSZnMK{h8ReY@tf~|c#ebWW%zhb5}tP#FK$n-u?$tB^qmEo zRfYOE^L6_97K*HYJl=xWA1sOxpnU|I&w!Behtu) zk;B_5G0<|U%VWZ7SQ>Z6|KYssr6fkbVSi52>S`D&MY^5Lon2CH>*9q@4=1tk2GLx2 zU@=2lrYE(%vcCWTZ!!d9%1}a*XtIbVLF?D-AMtD}B1+L0S^r6C!VBpHCxE9QzKq07 zrK^Mg!X?S!|6`{GUcqf&wN>E4Yd=2m9J>`ePn?95s~Z9rs`vlBP^a3e2WySxh*s#^ z6~%UrwH^idW~6>K?=AetB5ENJWvfoyCGlajgz%MSlnzNhu+mFN5Q21x!Z@1Ani)+_ zAoH9h{Y#Wu<66OOw$V~)jh`rM&6XaPmSo+FfoJN&*v6ccxB}n3+0>a;W=nS-T5vM3 zkNL587qK)B7?Pe4ByROUb&Qj@x-ULn^pU8N*spjs3)(^G`*A0u*1?LzZcNpsWJgkn zOmrdQ3ooE*(!mY~Xd`a1{*>8DQa?1N2ug>)cS-;6%M_9m?_bq5vtbY`@ zuB~JkC9fl%P#(P#HJ|w@Fdq~ja5)d0&Gzr?L#P8kf(IdZtu&;Q&Fa^lv&&^WKrtPq z7W>||kzgoM!Z+%!$*#dY2m|RfXu$trfF(^K_07Yb>+iDteLR(Wdmq*U0HL)faO(a! zZ2pAlr;2QZ+2jd{ypsr*thZukkT_sew|^8#o{EkIWJUHbTx4i5tp29Je1?(G%orSo z=BsR9N2tD?G9YF`uSR|&tdCRdpM-FY8TxRN7N`~LO<8rVM&Fr!G@LG*>f@+ZCpI?A zJsE$zc;=gVV9HnZ&K$^fgR}pj!>aG2s6?a)*(Nb>iHmdIlYcYya8%Y6-Q3yb=>Pzt zPqw~2Ae~+|r*XZOs5rSix&kCv zVxYU!%fJdyI&Lf_mp6sUw6@Xl3%BAQT0o9{wR(E)bWd63`ge=`Kh+VdBpg#tN>h@? zpY?P9alIFEOo)Xa2}$hw-2YBgq|+kT2YZmzCrFfC_h&oGM2l+u=ce9KU2sI4G8uFo zP0|7OD|NZrU~{J<&C%aAhn=5dTIb%_;}WZx%Ce5mCZSz_cP%E<)J{QTkFTWAvC;(TU$!O&Tw>pGmvEqq}eM0 z&l!_w%ldUbyqC#QaK!C%8T|ia_q@`s&V+-b0E$RlL9FqeIS;#rWk<{NJqF-8R>Bk2 zC4(^!J%z46{6BkOgQkU>p|{l+Woui@oNCmfmYcrn7K&_qeO>B(#YNDs{+VrkPsP&T z)-}0Lest+2R(#@I<&hQU@Z|TNbcF7g#uY-0q}}}&Bijl$`}3}(6Hij;4&FFRK%=P0 zQ%YBRmfp7mNuiSmV()oy;RP@=G61?q82(ZAFqXh&qm`!2uQk*l|3p>zOgM z{%*#p=22at$acc(tr|K{q&t{vj~NoxkllCI9c^hQL5e?<##R9SM{hH83DpMh&#;l} z%byMFPc_1E&N{m=49C<3?=fl@3 z(goGp2xJ>A3IBB-!(9DT%VmV($Ny!fb>`_bg!s^vLTU2h+;A4TX0eZ5J$0%%Oon{) z6t|dhFss_cJrf_V-jC89X_ro0ee(;AJ>FBxmabdq)fZKT!-}IRJggHWtgHjXJF#(K zwOb?%9Y3x~W~<(1y6>^6rGMW`|GvjvJ_@D zLiAD4LRyKSjjWlc#V3hrTe3=5dR&*|JP(q<`WqY%?)ob|JN55j&m8}3Z%ocr+c2D7 z%J9Ft0B$Tu(ms|{5w?YBo|m7vYt2*(9h9v5@Neo{Q)NS(rk;y&l43Qv>E%eE`6OpV z+eUT=AvCO{7j#EDu&HgSVVy7$EZcp%(;}n+6tBSzJjyDn%K~Q`Hi&Q332Os~7{FX# z2iqrVa!a!R**ibcfIA2aajtBvCs;)U*4OO}P3sTS4vf1K8CJii42=Bjy8?=_EW~%J zEky)2)Oih_c*w-36#8te6w2*b{g(RX(|eLW^8C&MNnu;BG=5syx>o}%PEY`tP69iG z#756=*_DK8d^5y_%gg=wl$P)K5zE*Io}SKhf$?u*JMS)#3n zLrpaJR!E|z!#L(R>$1OoHsQ@@;A-;P$S)a-6PXjA-HrL0oL;ornvHpWpSxFKF0bl3 z=eTc$-X6Sg^jO)zeV8i$;aH+&*q^4*k(r9GE%ZLqggafLu%3MbRk;Vkq9>c!+hR=r z)R=AqT~Nf)B3%!?Q9zING66l^me83)8WdWY*+$u{1*1J?4JBwXUOAd-JQUJA_DR;3 zNVymt{Myfqpv1Vzfa!_98>e`+H0^Ao`q+0EcQx{v@!$7+fV-y4T`G4YdCskVN`)&C zjroaDr5>FW>bEu(#AW2vS%`|KGx&cAzrF5u{;A(R_-(`a_8&EC-m6H$_o|_~O2cNz zroroC%=5~)0iL+-5SWUg=b?N@6nG~Tac~c zkQG#!=4?kfeuB)Z6iwUT<;X>NcmOJSac$r>3Nf_B8F;(@sL&AbSjVG;kqcBdSEcgr z2_$61BaqeBd`5k%YrVhDwdOr#K;&nSF;Gl~lhc{265qB^%hXN)2NeWS{nq?+^tLY5riQS%9p}|quoKbSVdYEWyGL-4`BnNl^dbwRx7Yp{XGiC}W zx1EaZgh?x`v&I10N}pTuzI+vT&Z&@O$y{vS+FJc`Le@vpBy*~-zIMF&kX}*l=QlmE z=N!^AxD{^35`D*3y1;VPz;n> zqd|(;3pBvj7cw&wl61cVpI6x(p&SHjs@A-lS6cCGtCB{bSU>=-SE#pP4PZ`UeZ{V) z7ku}wC5bw-z~Af(J;(G^1!lmcQJ+x|1FKP~A9Xt9k0vGC<-%g*FT&P1{V$&jGyWI5>Zq<4*h^}wZAs3PV4OT` zO5(gJnxG2Z+mH=xDBpGUjlOJb9J+C3dsC1Q!?$dewkk*e;AdBC>f2n7I|^w$CPupv zGEIBuH|{uD)g#^rrBMA&YThLae$RHL{|CA6C!Eh=JUL}g$<%!FGJi?7iy$(w!q1Qgh9pd|L-wLjL*39u|f^+5hA&3>*rPibQ3p^Pk)esce$O z8HB0C)6phsF1&lqd~y7VPQyiSuzGy2{b_6chnS{@`Z>qyG?J=%*KpUA{k_H)AJe!J zrDc0Zj?fpUm`w`_(a{oKEpZ*Q#zA|_oVXfaF0 zX%D-_zTVC_`@vdd^7q6SuHe4G&$Z5cbce1OhYkKg`gJxJ z6AlbUb$NxaZA}JGp1-(1J@a>V(inL5AIjghKMy`&E^ifP!o89Tg1fTH;(cpgxYaM8 z$^j1ZgEjR)s>sW2`BaZdKVoa^+3eD^=zET=DY>u`qkc>AubC+a)t?S-c@Z)BUWmXO zFJ+6o4gL$AcIdCoBcpH-hA3@iJJFzIH8gwH8*mrR1rS}LA2+qQwVEN*WR z{KZkgR%zK_?9TRUa<6BhBD1Wl6Q6zTnSh&3SF+jZmHJplgxPTil;kD4BgKD(-uSdL z65wakeFI}2#G;P-Q&F9o$}pN`YCbmAqdt>u`t<&xOYz7!D9dD0CL8W_ zPW(lWe#Z2F0{l5qD0y=*r(b9?ZhShx|6e|T0KPn1rPDy4W~Z6UTFs_VXHT}&xtqqQ zUfVl&jCQvdn3??jMOMaFJOjA<$M3OsTxklG*YVtK?Ij*a2$SEXG_V2MP;`3spDN5> zp_s!Jwz=&@?p?FexpqZ?)@w`w$!g-`Zz011cu69$22>P$L0jct*~E*yBsw#Q6YBzu z=`b_t)z7!WK5Cy=@P5m0kTP3QPFzIkwv+~>=a@3`#e5Aor2&?(?;G#j;nkjPIRX4K zYz+H+tpZ=w#|8iMyxSEvidfIUgizT6n5kadDQzBz8943%HKYcqr2eHb5b+DFct(f2 z37fj(tw2zRCOe&0SOS=n{IH%|QG@^FXyx(DBZ7~+pXHd*N?EzH`$fXx9?Pz*@m^tk zk{|iMswrfCb`Bo-b^97V7I9t^l2UhrXeX(^4cB-|+Xz}NK>ZgZizrRlv1$XAtd7w0Y7CPX7ZN8UkKq1`Y#RU(^9S zItYB=1qYDIOBeznYUVt;Bj)j9hU}~c9Wq}9C!~$|`xU2yq5egiquW|5!EQX$>?<41 z+^$W${)08j{b~P?rf&+2t83eCY-?gCjcwbuZL4w8SQE3cZKs*oW`o9@Hfh{gfBL-t zcRVM1_PTZ9UYo&tSizkPc-@orC>y>6?{6tu! z?9*5~Y3{YTb%V49kt0Q&BqKvX9FSERArn}66^fQz0OFm2Rq0yY; zSjzw6OV*O$VR0zOrSl+PreWn{*Hmd<5fT3oSAPoY<@yJBQM1Zam60{qhd=TQN5JXo zFOp5MdM%(XD3o8uo@tu_z)FFvGuc`Y$X7!9;ku_w-#qBqTe%P!R9Q(>oguHIQk@w} zGP}BK(U%BK;>_a#&c)A_&9UscWU8vZ#ptK6EV~}GH57NxQ0O7t#wx%F2#ekxvn;alhqutNXGI{L5ho; zGmf#?A4JCKko(l?FH4|UJygk5ZU(`r-(|_u+SN$7CmtthaVTb!m0?nmd|a_QNv)Gy z<^+#!{1czV=f$`8{jW1Cjw_ewC16xWhVSgJ){gJJZ=7)OTAylyndIT>*SSAlH>{Ti z)lYL#!7~5C)$?ki`&^#nZE=-na-Ys{_4|N+4`9x@K27P@-7o0 zzi+9F_UW{3nbd1lX7AZbV$Zz@npfR=UhaPtW?XIXI~aDh^CD!-Ijt2zYv72l8h^Aj zYk09ch^No%0EaU@e3+h_g(EE;aD)GG^A!9opEL|G^qv=+A=C}gTvlliYwp7GcSCSm z?K1Sq6;-WaS4vKw2Ud?jjgOk92YOsInekP1G?!0Jz&-iYxlxH>(A$Jh+A`9tDAEk@ z_O|8qsvyayj3#u6$O5GtoeDw{=73lQ2*B+-lcuTNr5=M~NH7_#d%s2*b=ced<^gXS zFQ!)el}lbL9WKZb@cr0uR@e)FC6ohUpE2D^$no{Mg~~!;qLz9_=vz*|d6QIBs0XS% z0+xthG$L_nL+>XR5|&sD-DZ2?;P-|d^J(fm0#;P$~dD z%qX97MZKQW;;Vf=$K4C+ao-~zemh+d#hAI=PXQml*F@j*$1O!mzB2U)M(qFUJhupRqf5l2G{0w?#iwdlSWENCB0jzAfLT(eTHWc&n@ zB}7$ca|I^9dC2JTXdeI9yy{RYT3mt*XrxDK-+zlhMlpVxz1{eENlpVv zS*Xif6D68Sts^0?)gFVX40?FKRkED#;FQGb*S0@viA5I>E%^fYrxg0xZte9{@UrX!~;WB3ZrQ(w(Bue!+RpjjfTZ@vx? zDlkVXqHtkWZx;&>5XjibFvz8(Jfu;1v|3641Hc9k$;7f;C3G6K2TY|&vyY@)U0#?if&v$J(wpVrg(`R!bs%1f%g-L)zz@L8$Nxu>`zG$e4g zRUW6wXAIL&m0_wYH`;yMq!zq6KwnDn>MpZLPu6`@hmyxxdD{+i4wJT&R63xI`|e>z%0b6&9EJIr=( zbz-~cT^GL-3+iR7a@Szy$A7Ma|{F}^s*`7;wGTJ*$NqS#Ys`EHS$8D>uf%=L> zD@#?zX(|;MaI28ksOU)?CC5YodmFoKWGp8hX`XGM=QAYmP1g&PS;6fPaIx0xk<|C{ za^^^S2S6wp84VvJpsbt|6FQV{2_f7GK*h`dLNV*(h$0yIGm+H#*X)LEGj68mE{-7d zYOZ(TZl26A84|eZ*K6N|tPWaLs792=-hvI!&A0alZc4ti_!LNUlpnRRY`Bz=)x2nD zR#R|0UMRG6xf1z&?2NcpL#w@tH&`l^MsWb5?TA%^WybVl=TQfI>aU!%-tvCe4!<5o z?vu)lqIriu(QiIpw!CZ*qp8mb7R%@RV>-my*$(WEw_I*!+-g4L5jK?cR6*14_9HtV zM+83;DQ)nmD@-YB;wX6xBMS5ArkZP2?T@Ewf8Tl0)>rP!iYW!+NMXj^N4f-p#d-rT z4ouTE6{zHVy2=pk)CW_OV<_7Y9B{p^ioA`*6r^0YlN2J?FR|x>_X=z;b|RL(T&J-L zJ27_2U7lR&R!gg9vfmS5gmRe3q=zr_DeAc~@74R~A7%&yOFGLR8jA{XL&&7ta|wu> zd>-Sj{w^V@to~zr`Ogt0aM-Tx*Q24ac$l#m#rWjdRBy+J9PJn2-e z1QRw=H~epGfWq2H`8heyfWs0hb5?WsqoUAi8Lg}RFxMk8o{Z$y2r4Yj3G@ct0`n>A z?ZDU533oBcp#x5M^Xgc?OJlat`2}_Jvulz|QxI8Pm510*e z1sw+n-JxsT4y>q4L#fnZj3p2o@49-5OEy>Jj*cQJ08?>~(BNKp41Y!fd$m|9!?oM9 zYLh{nn*J`+DR${Zy`}qNiOqsvjG(VP(TseG#aa={X=V)CV(E`MArWU#O9~n+uM}a)jzb zXiyaNeMOK{B|<=L$iHH=+0L~N`u)9y&8I)|^fRoNX)W1_s3^#Nl#W*2p4*nWk~d{o z)!E)=-5?0zfPFYa4D*ujgm~@JcK}YOl&)uFAHE71H&pW8{oZVU05f=5OgJdE&W4^`y)RXallko>x7=mEI zx}vHs#gLJI@RQf;C;;;S!(t+Dta=O|9W5nozEx70+#EtI`tiSo*PVT(TMGduo~0X&B)#bJjs^fRfNH}CU5B5O^ux5qvkk6VE!rYuDa;8W z3!@81(&BVRbVXy;4Eud9AuDnl;TdS64Jo4Gw<8wT=jF}jDKw-c<=9M}#@Vp(u5Z5| z@5jAJXxNEiih;)T!aPt<2ka8Yc0oz}iRGa_P}Nk$xxHTyWYM2B+t_d!zZQQsjUVBQ zVxx&J)ppx&f=~Nd#dJ~_Ng*v7vFTgt(=1IsMwUf3zuyE43LnIE2+vR-^cCqKh`q9^ zK$Iu{<2RYkd58%)`z)n9;%9s{PpvL>M=jh7g{8MN!Oe8S!3M6k{sIH7QmMQhGr&7R8ufG2Z>@ayYIQ1{P zU$BaL-elOQHtcRKK=P9wB#cZh$#2@cjPM$}x{W73q=%oJo52UijzHzI+0;hIu8Z7W zT`YYc^D*ljUghz+-l{uW-ZYynwrh{AVF*R4DL=ybW&%{? zzu*KK6Jq58DfWb!wkPYliQ~EA+b(0PX`9NM+S_0+hP==r958b=pq-)vt7E5Hn|cWd zNgA+MInzpY*?0>g48?Dw*?dqaSGB`8{SA$5aUure?WohLqtV2h0EvEy9^w?R4X_sU zVy4(5KK;JXLy~*wO2DM?KS*5(|G@1qlkVG_w;UD%h`qw}}K?v|@MskENnWw%g6sX>)t-P>*cM?J)MIX%KS4X&-;V zDoU1sOEt#|Kh~?zkG56^LPkmS-1{SHNz>f?QABTUY)4-^LG9k9LEg_at#c9-{xg(> zEuK})o$D$cTF5QOrfvAg@1#k+^H2GC$GIlb7<7jLAow=^ptemE{WW)rZPZ)|eK>{6 zPS6CswJC!;-y|^5!Rg>VeZt$N2Wj7k7wtB#cWiugx$We++N9vgWBLX}UhM~#O({jk zZR=zX{pMG9=Lb^6Sj|;NOpCTMFP68*GTf@N{e(=j#z5%bBt7&PJE!#eU4s`^YrsG1 zVFJ8snBeUNfme%SPF@u;JfEv=OV3x|^TNehwy=f%Z`@cPb0Q7g>8O8Iy+CSyt6;D` zA;LNBYP88Z%|L0a;e>9XVBsC zHdizFRqJXU zYwo`GxiU@Rw#_fBDE?P)S}^kZUS6B`u3S_)kQzUNb|i(oUT;ywfgmR!k*l|_Bz;bh z@4|}jCO2{{Fj$5GDVv?yfOGs54`o5k?{~0&6#`!f9t+X&LvK6@ zMdvBvUXAO0Q`D{&-o&8Own}Uuh4?v9ud_gHBK}nAjjE*hv8e=WO5`W)F}~F`86(|| z4LThg6AHt;C%zN&gYEimvfdvG>@*|OJ!(IMpl5m-Qgx(ES)%q2^Y4sn-oVRXrU(jr z!|>3G*lH=JXqspr#>ma6QL7O+?^0bLRaP-*gdmJ%SQ#d_z}|4kizhfS2jJ<{CDyJ{;Hj!LWIcgl(9FexY=q#0GJYRdNy@?E3|^!}(8c#x17W zY&Os2hq%Mn<^T)?ac2y>U$S|NueyBpLfhdh#9`fk@ z^ek9!k5NM<>X>^Y7W7AcA9io2if@)uHW~{zbrkA&uX3^9mq)v4tQ$brid;xvjgnAK z#76H2qRUhm*Hjh%)&!{n0V;?NCvg;L2&=8kp+D}0v>tNkxR+pF1sf0vra$nQGxH}?B3&gV~)y|plK zUuO%o+_}%`{=Ik>tc(87D+bv&YELG*GQSVGc0+x%yif2g2q;X^8#vPhqp^=j#n8=T z8+GQMTWC6APai|Je<(0}%4-_~h;bk&5w@e;e)uJjMs(UvV^*(RKUxqWi5`^pmW+R^si;kJ|0uEI_8^BBAP||x->Rw3A(_M9_W9R?YP?T z3~At;_s-=;&s-Kb*nNM>*D&=K6tsZ8%6w*Hyfnw`9Zi(?c@VGQ;#egAVsxgEzS*sH z_&vyv#M{LIJZI8<^_65v%^l<`C9=vyx|?5D7f{v_x1D@FCC~)_xAtXT%<+-hR|@o$ zL(zEl%{gedQt^?=hCjSxy|dyUZ9yc6bsqrdDDd&Y2;{)in$BLa#Wq^F3Z3LcxYYBm zOD4lmGH_V56=Je1kTaY|3&ui}SX@Y;F^h^R*P8WrN)96;Iq$1vVrZB4|9JseR}>SH zB6mdt4wB85m!k)kx7@tcnpl^WVy*lUUH0yT=0v!LT&hbN-!|_7wAuue^6pFzV*1t4Mf33Ww&q`$#|V(P$$un;M6&_oZ7gRmOA6{OOf9i#aR-W1 z!rd{Ma`U^!08vV0$(YLO*>LR~%g>NZ4gO55ffpGv|7tq>)&q>f4$CR&Yd+CeBv&zy zQ7a_4F7pX8COOY8(kb9khKdMx0i1WQ=farh^`I+41*jI|TJmlh0c0gXi}uHM;B?Md zGBXeK<`Dd_-9R#!aodJU2$Pq6_xaEb$N#h^N!0j^ z3G_R&f*d6Q_9vS zih+i=`= z_Q=zsSS7~Kx>y8>IXOA_goH4ZK18GNzULD+$bt{KstA9`n&`?TtE@>3I_we)~%+nC0*TaNIKuCp{WUeAXVU9j6s^cICgr(tJUaCZ8JYIzUh-{p`d34AOtRzUA9w=w( z)@`bcA5Qx=&Q4;|k-ltMYutv+(r9+$T-`>`kjY$FJg7T!wZ=hc>*Qd4oAStO>NwbR z{s`7%4TiYsRuG*$nERWumWIP{thRt{VY*drG%Zx0+8#R+XLhh6c0B7;&X}<`&Bo6; zIM|iP8^N^kth=%luQPXiHZ*XSmW`yYN%oEV#L*rQyK)roDob?Jh z>YQ*hW|&;Rk?#6pCqnC`Of;}1g-VNd?zp#Hm0AAbtDg(!t47H~XW(lYXnRf$Z5CsP^f?gTZ<&+F7$9XGP?B+D7CVufWfd;O-y2 zoh(rtNgXkK&_Ym2&9LclyB}rPHh!rPlJwjj!kR=mieC8+)ju-M>8`$TM4A zy>Lrc557NjxLjWC1PGw+wG_O=lD_qOO`m?gc_}yk@`q%vO)os~{E z2Ybw62<00U!l<}YEuwIVB^GOv@7OP&bgCv`LPE>+u$JRyD-Oocuq2LyMs1s=FKRUv z_aUv{?So+Ft$R+Cp^Sql@`11Pxv8mZ_*P&TGHU#;rYNI*sA+XCti5 zC*ncRwsVmlN*~}Gh*uDN&imJ@th^@xwH@N&Xfc2_o+S}X9OAUgF=cV{+c^`U3ai;9CBh8Wc&%SCk1O2)DZ?9J|a*mt{i%i9QkP94f-k z=(K$&RDL|jEdg$XG&)~U`b$*mOIq5*@7=ZlY=8ewA9NWBjBBlNaZXx+bCn&sVw}2R zb=w=`_x3Q3z3RwJ45ggX1#9M)4b8;n0@_`6VtMePB>0ey=Z?DY+FkGPE0X-gmqRez~e`^n$!^UwtToX#Ly!a2Cx(^wK5el?bFeV)twJzF8%ZF{$U ziK3?Q9D0}`>w95^hP>(tU*zJ$^Rk-xg^*l|qBZP_x9MpFo!7zw-pojZsxEd=c)3ZK z_pP->lp+QSA!&qeWp#X88d>9!Z-NqLwJ%9CtQL8b&U; z-6wKlBK|`{IFjW`>|{=$v@qMX>mDvNTCUY!X^I(FjZiZ5FOp)2U;-|el?sYlPdPI0 zkX4%&POs(78F^k3$M7y+i8*raSC?|HKIDDh58ncR`NWww*$2@l5FG9SPP{x6#kzmi zdcEkmYd8Ye=ADJ&yV=n(T5k$ zp#|>s5<$xb9ia)kOuLVo8k+$Ju_}!IMG5C+Y+*YAfTvuvVOeX*UqFw(T|0M6t z%Ntu*mD*FQ)!86vTIuz6i?k6VCd+Tlred@mTW%sX87xvY__IhSY1B6xYxCU^wN$yi zeh6GdZGvO|ysw8?yl%e*^%aVeacwOTB0)m%Pq|{RnjIv|Fe1R+*ECpT6WyQi`({0S z9YHzKg|_38oMtE%{ge6YLE8=oN*uSnzl5&DmOWBkp762mj;R6?KsxAH(ydBtu|U%g zwSrYxIKf-qDn)U5Rw>SM?#TnUje`{dle2daaOklBgd%?L+A3bhLy6>>_w7z~r&D^*U)m!45 zB_KK7fAb8doqOYUI{NU_+@sd|J$&NJfa&*}HJu=wW8}x2R(2Bbb(#5GY2w?7qa=#IK)0UpPmmBVdYEIT}RRxLF`lF3H=G-?2o$HYtlA}!z;~9-ap$JHA#$_7b zpd@95j~@-;?fT(WZDkXtkYq$Shb zgpsm+;i6^;r`1gYG2?}u%@xR3@aY#CnH^Gs49a>S*@L1SW*onnVMKB^duMlIUH8*( zLZSf%U5g12+I~KLHL62uaTPv$0Tikh`CK{`T~lSv;JQO1_!&(E#TT9KG+|@{GSiO_ z2}Mj(lGRv^NL|poJnGGv5VIqgpd|hSI%7*oYNKPaz9GO@!NcjjTOB`_C1+ zJ@-mG-xtUBuqrKKiaBzywD^>ee>yJvyBy1wHvrSzKzZ-xklfJ9gI|Ue(nrNhVsf7U zA68PLl8em&P&Q#g)3<%Ty+%SnsiXQ#Vw3jhA<%Wcc=MHB$0SHtn*EroDVqPjG8&tl~`fyeJk>qwt~R_h044^Ge>7 z3vW<0fMS`7@>PtAik6jZl?_+bKWMN2AZ_5|J*mom^a~q#!8ILTU%832wcQuqmD)WHzmha1 zzVu}d(=f!n4(r@}bSn37)^c&CniT!8$_je%_0M!tgbsVi*lZ(ckOR-T&??6;0SN>H z21URTTaxh4sj8S!BViCYak~S4f(QwSm6RAN>aaDjH{zjEKo@xNZjzu@s|<+z<9!MgUaSk0SHMCLOo1|K#yC0 z{2t^ur>h|T{k9LQQm`hGv-Kt}yAC*Vx# zIzOC#H2g5T>1ToZ6c4pIRvl&%)AI#=qEufq@VHIL=TE)RUT}q1Cnox}8wISLi5pVk zoVQ;lpUmoi_up&2R9W|(_hrV}qx0YEiX8vvl;0Z$QcH8f&zAIcj;J_c832MOfv^bs zs@sVXuOh1k;!jJWw$*GbCihQZtp@VfHdWG8qAB-{$~HhDx^evSS#-^-qF;$5?V05R z6`ZtdoO~~k4b)7J)CEXuhIO+=)DK&gvnlwtNMPvw{wjN6rD$!-;jDt#CEW_WNu#o>=h_^~RcyQeOWbnNA zZ0@WBPH}7p-md?>MM=q+ z#`vjIKqM*nZF&reMb15qebmRDbo&hZDh-&FC7y;wJ+@DcEoQ5YbYY9LpNo_3GUnD8u#oVt^(K#+;~UlmD!HzN5(F3z+4cx^0ED>Yk84C@v#>{!0Vdcs*i7H! zk^9IBBT^8Y1jnjKMaP#d)OV36O1VjBy#^++MaT%tg;1m@Moj~HkV}NsiJD*Kt45BP z;jM9jJR0X{%Tg2H8D`O^gQEwi2EJwCol8164Y5@bup{P)o_kD|(d168Z}AM)Zy5jx zh)cUmrX5raX5<)@ZQN9Nn2zLK10oClM$5S>#{&E26Nu!^#p{b$gxSHB)>@G=G62$= zzzqwZfGKgiSP0)@ zum+C)`FNAZYbn?W|G7R0lZO<>pL6DPgKQg=zEAP}6LMmupr#%8<+p;Khhj;pAK=E? zKdsGAjec0L?D%gDo4bRfNr_5szMB-fANwk*a=vGijUR4PrhF1Vb_X%J zE!BCabh|Nk{~|hW?SRTqPcc&*e5&j2yy%VcS@Ozk3EFC@)o%+8prB+j$X{;^nK5Yh zxN}fGINrE8Jag%SXIBV*NB!~_jf0ba_FF|>xK?6YQLeeAAXvnu_nUiJdQC%T_^_y) zkTR2bsB5YA!?8EXet2p|W92jNwe?3JTi(<|B$%{8vzu71!`M*H+xDc>)ptM1buaYR&1Qlnl+{6<63r8?F#I?s?wOiOu#{jOL@UWIrroXJDht`c5SJku@Hp8Fjh-#D zqY+G+G&>TdeyuDsY_7CkqbkZqFvmm|PoYYiLP&2etFmO(b|ul@*vQHPSh@P-)(MRI zIKSeyJCa%!`>bdBqiv@Guwo@^i{461I9t)8Ui!?be!|nm;+TQ#NSGWl)R;1D{y32T zm$PBTxo=z)nJOxtI+pDm#J<5}r5i3Lty=BMW9(zipWwx?J;E#~<+wYacjS$3gvYm0 zyY_RYLS6)}>hA*H+v`-cqoN#^!Ih}W(0?g^2v(t-Kdrj2g6=N@Z7@Xt7Q>x6WY%j3 z7Q2ky*}xC!UI|$JgLCB4DuVe^eGfsf9Vp}#J(1ZeYHqfW+YzC#EK+v`ekty;8ewDe ziz@>+gzcB}v$WqTtquKu=%oQKcUAH&Syg_K;#*CflamC!2u%NWKHk5X9WnDc>57kL zO6BhGcuzDjeLl|14VqM!u1>H&7Dbkzqy#1^HM$NM=6+^wQsf;AOYG_Am=hkO&di-K z+5tti(bnz`-YA!qi|<@Qs|a_;^d=Z7stzhHC#hqZfNn|T5&klsm~DMWsr|*5fHZ3y zSDELTg9Af0>%Rp%-5fi;?(H$|J{6sTLXeW$P$IBt1! zRw}@z>J6cc`*k@l>ZdI=s?`X2)G9$-Q$`97kYC-QX(gqmq^2ZCL;H4qJDWl&(tT0h zo36}|Ik`NeH4Rp#7=9F=y#bi8y4LC>@MD1uT}7QQWpx@k%ydn41@Y-zN{k`*gCrTyK zSGXIN9jJ4Ij-cNmd>7Acs~uHAMO6-OC={hE-bDcce`%q*y#+zOQ^)b5%IFYiuQhD{ zNpd-y9Y7}Gz5fdWgexNRX|u~AlI5(>sXp_5qJXv1wwQ*F1yoScbd4(Z z9R{&)Qb~Jw%v{FtbkW#z#^BKcKNo%#r;L^d5TRS*W|xDjs%vKX{Oj)DnpS@L7ku2y z=yj{wNh&O*mDjWK^I{w9dFGM-3YwGHDZ)*hPbQ))lEP8PbfU_GfBT9%l`;^Nt@O(Q zkWr^Qk#HvP%YhSsZ_k#6(7=B~l^^ie4fyYu(V97du-*iV3DdA8Wucf48$y$(T53hX z`t*v^27tgglpo+Ix&#lUfH%}E)^7oFf4G+5eXh;C969X^aX{MmXaq*kcWHIZjNxMJ9V~o3VG?)|i(*Js z9CVO+(pPCHk7J1f);K&=_BFt6C0EqqK#PMdPY$<>*+C?V<9cDz?UK#2ZHGSRbG8qf zIZB;&+@^1?&$Y6$i3w>OF4|(I9xNR0<)MU%`*^ihZLqZfLnh1KIAwzQ`abOIOd;xK zPsK=rk-zTyUmEU>-VbR&+VwLQyg(!{!?TE8yetT{g8tU z;C1YeZocRFEk?WV_!k{V;UeAMO!qDq1q&eOwjEda#%HFGJKP4DJ1DH<^P z!BMboogyjcZEV@aI|8;ins3Jvg&(>Edauhi=()VC)iwV8$>l6UNBcIB%Y%x|3(wLp z+k{TxS_E>!(de5+Y3#FjiU^lG=}IDMnP^SuI1!U5 zT$UA(LnmlL$u53J_#IH5gx(N~q}(4;@s~uC&C;jf=13r@zubwkmEVb-Z2*n+Vt$;U z5>deW%zE|mbB_0)@-4T+E(!Ly(+9u{B)0;h&`7tkB8T?JOZ(-?f0#$)5c*v;Aj-NW z1Y_^;V8wkhW^7>xo!go4c!8G3buE^C7Z;{SRAR@#^nOscc2Xe2(iOa zci4v1VDw3-1#Qa~aP-5$`)ahLeMUCo{whk_ZRS7-5`(6{*?P8S_5Ao{$lF~u*oM1o z)W|Y+Q)A5SX!ipKHB2zNgN6oy_iw4ewsx({dEKEuGtk zu);fsG5*N?)nAGZ)oTAT!XxZ#=nPG;x#&i~>3~exO!E|Um7Wat0m?T^ z&t)^}&4?{N{=vcHPy20T-I_RIVs6n_`@@{d+Y!xUzOmvE5f6JGd5qvUumx%=u;8h?DI z)CX@i9+C6;>GvUVyW{#mLf+7yO+M(+6n}I=S*0ASPS2=0I_-lwSk1v+;-c5J==sFO z>B9AKfER0u0|w1v<5zz3B~GQ(g*(1J?-41Y;AMBJDv~xq9(%!2E8YW90Y-!GyF+6Q z1eFN=|MIHR?Q$=&W$qxEiRTA>f7jS8W$?}ac>yT9pC2z@YkYiGrjY=0t=|xc61p@15!*tr{i7v=X&dqj7PNa)aX`B zkP~18Dn0&l&e_p1Sjk~6fzXlN5TP`x4fy#O2f5p!Nl$q~4UG(Y*3s7Q3Xm4lpF><7 zm(H#-SH=(cODr6!<^FrtwZ1i20GN%$`Q;7@**d>PS%jQwU0yXTMh&F!T27vNn)*Gb-5@)wCw zwo?*v^eP)OtOS$w^Ua{eXr!0AtSs4i|JY90K2wTC)l{bB6ZL+hBK53MLNv;zF$Fe? zZq9;phg;VWpA4mgsjB-<(L!SqK8T;+SGl4MF79X^M?tsSk2-I&$-^fNg^-7%wIbp^ z_DS@ennfH3jiwYmZX#5tSezylAThlF?D(YZIUf|G`m8&9nwKIw3OxUY)YW=11GCI8N>$kq7tpH#m_SJ zl;pZjGk<8DLgn>1l#Aua^938KUA%m~-?bD`DQrU)1TM8ha3VFdj3AKVmxRKur7 zbS!KQ`C`M869ejF1Kf4^6wG`EII2u(+6aId` zf8)0#Z$WM1wR!iv7?je=djJZ;lCeh;d$d$7`5dti^#>{w;t9No3OKEQ_SxwUiY(Eo zTeEp5`AGDC#j$5_A4aD+@bF8(tJ)WrJ*|gJG#$5tC+cTU5s^){{^#C6s^GuHw}Rek zHQ-JY-vQ|iZud8bQ^+qJC?DsK$4GoMQWMD+_=9y_rTV7}V+M-gK-td2iy<@L1wudK zRi?f#3N@b%-A14Utp-oBw$>+lvJMxQL{JFKstOpt;LSn8k+m9O#^vFK`!zH@*;ZL;)OC-TR-eb zUUX7-(i#DwJX@Cdx;FhLrw;-FUAqKX)FOZE5J2Hvh+^+LMPFYFswj(Oib2>v;v%0- zR`W~=yVk6Z+M7fwE^Yf{?Qi!CV<&qw1>IY9?4#h^kk8WAcLQP_qn7VNFX+8pS%A&9 zI!Lggj(}_1R;TKiTxY3`o^~Zd&Df?hngs`8f`A`cXz|ZMyGUJyf|-G~p~q!ypPN16 zeIou7-L8K3hvFm9) zsztVv0Nc+T89p+B$I$yZ(9nO$`wFq(VbGiR+1ps)==A&lGMSgG z^Vf>N!?FU2<>~wlswdqIzHvm~(|OT{hixxtedjLY2<@CdS47~IF5z4Zl6hRvCHT#H zd-nKMeF+o}&xoJvQIp~Oajko_VBmyHoo<)qr5DLNWE8V&b)>ex-pZGnT5qWBQ&F#k zDBH`tAT)M}2K(IVtu@Xl@He_qp69KY8snRK6?|9NEA4U_8Gm^1qj>zj8UICo6sp?DV^3nzy&%A)AEwSK42W17B4P9QaoNHYh6K?M*5Q?;ljA z{|&Q*z=ZRnBa}Kr+qS!}EMaWXRHl~$Ek2xYZHc{KFtJ11F$4A%7N_EO!C;1bXVD`9 ztaGQwUr3qbg`%<{335|)Dc05bj4i5H0}30Mi-rysadr{51pawTS?8yDXiifWFHeiP zDY-Rz;rBWylZr zUPyeb!F=`CUo2%mv&a4e!`AaD4^=(9VQUA=7#5eK;TQ?RbX(>gGcbIsS@pJaa24FcqUoV}JBM`KWsc%=MzOEHWrZG9XtA82 zwa85Ouz1gGRh%X_U;!@kTl7`xi$my*t=&s$FL zjBc&|P$cP9VsCrz+|8{_=rhrj5>^nTZ}J}`s&o@|y>i;wJ;~ghJ1)Yy^J8*2KbY|@ zm}vtGW`b@=r1>+O91gnbhOnhfzR2-iQ^v-qqsYU~Y#`_wbQt+%2ol<*&{z)D}F~i1FitUMVWX5oK39Z3T z;oZRj+X}}qC4Gx8gIVFJnz{rq0QGpK@=nnxUQ^( zz^X&-PYxjIyfyf8%)QS+O`RG_-QCP_Yj3MU#~#LC}=aZ8FC=oBizTJTB0!x4bhq zX~n?zR>}1jh4T~d%Y3@0v;Z$&N?M|@J6}$|Uo3~RK*Cr=7I{QS-f-(Ptm1?`Ac3T(obg-DHmKRn<8){iv^n#f4ikCE z`mGmMDYvfCF)W%H@=9cbz_)H!M+xU`(*9g^{n-DKaEC3Il6GG9!vE|zZ{3*4;J#;)+AHiHNkDm$12iyDYDr*j7k3Y7xUM+=Jj{I3 z{M!Yo4EXq#=7JB=h3DS^09<4owATgTD^^>j{HlIKxa?N^3C?nY5$!}HUHCUX4>@TYf*80~RPo z+?C*Jf7wr8+zJ$IwBwz;$&uAgV)BqY zS7#TKr&l=px5I?P;&Lg_M6EH?n}mMK4F-uvX$OxAE93B%uRW~8^P7${^4MK-afsh! zjBy>=a4mJ(5e*Uh!hp-%h=DJ7)sE8~aCj(=!?k{HXB)xO8K6oX<+fcbcY$)l&Wu@Q zFH-sp=iVBh?bbF&F!u(7xbmyFT1Y00=@NyhqbHG+#8iZfnNB8@{{IlA14x$kGd=yc z_=`Hv!-5W73-^;0r`V3`6Ttvc&P1)s<$-PF6?JqG0t%?8XZM;MCqdd5jH-{p7~!Gw34gn))lq3<)f=sQ`?!r1 zcbF~cGT3u?DwTj%#$xiJL2Q$Hbgn_;i;Cxd5e0XmEY=GI?mvC09``7n4~nJ9Ry zIung?%pRZ-TWCP+k_f5K9-`aDQeTZ1pB=Z*{jL(R%dLs+T60WbpkF_N%>r?dI|-@R ztyMuw0GY#5&rCBV-7>8fhok_ z08{eRjc<7%qX?|f~t)4$_a*=31r(#|JM5;$;Q)GVp^-GZ_fKhOO?V+57#Fr zx*nk}w5uwyP@n^+#88*8&f}z)50J$IOV@GrJhJTJc*Mfe$0Noi^p?;T{ti$S>K$79 z4wz}1KNf@OSSj+ucxpH-jga`B50t{$n(V|8*pRflY z&gNevQ6l4M;k z6U8>iyNrTrEX0Z`diI9JeizJpI{IOlbpbW&b`&Sr$IIUmNO~%9_wch&UatVfj3hO-VMsD zMzD&p`Yp!m72OpO`u_Kb_1Bxm#lD}~WHcw`=|@EV!6e;sz8v_Kyx6^sn`}gwAlE9sE?qJ}2T;WsMP780@fF#$EK!C4pS_S^B%33r}k3HEp{M1$8YiXz{ zb-`vr_K&;)Wzt|A$Mser@PiYCi&YV?>V%=Y*--kV4_*(^Hfkq>SxBM zQmgjd7@a)-+y_z{ntC1^AyU}h+oU)UNrsj{CNZYE2aVT7@Ma$4_@LMtFFol+UrpnR9Y?x}^d-Uu z|1%sMTZ=`(Z0|Nkh&f)6ysj)+yyAE7!2-GCQ8x zCL0(5({*DTDnr>$>kljr9tO+OfvES}@28oEPO=a=lHP3D?}Ya&4q_JVg-Znr9nBSV zMO&k^)TkDHHA;C0>6T>g&7ho%Rup0W6{7FXr!S(2P3yDLY?bR=ujdyi7tL2c%Cva9 z+MIyTI66ST9hdGBF|t6u*D$LwPizVgFF{9aPI3r_dH+pl5#(2fOLNSpzK*N&D6_2! z%38EX7)O3b1o7oZ_d0_nut$o34Msk?FbbCoRh6w<&RMRe&YSRV)$sX> z#$9w<64jra7Y+wCx|DOE4(^v>rNZJ^bHn3PHw!Hgt!tWS=I@xNfe?*((efIH(p^bnYybS-%_KFmfT6e z10988h`OlA~vPB78l7}ze+(s~XDD6OH>r|MV zv->JvvCy}_XZckmZ9N}jE#MnK?d__~7hvehe(v9e?yJ*ebXdULWF@E`5~P^(o*ub( zB8_sd_Mt??l)U5jg0!~zVJ`m;$;Gke|H5y{)f&p12%vji%m-rwgE4Sy9Jz@#gF-zl zTL+Ifzh^UC0I%;1P)>Zz;d0o`l<&{qpq>z_;hHU5PZUFoW#01tN`fW^z`4vRdh@+9 zn*#0M2-(lO19T*)d>B36T93D`CF6`o_47924S3+#Ab@JstBm^xX$Shr0;dWM5a!}3 z{!Zg+h&%5K3ouMEqL~cpo$S9vJ+fX@LTVFw*lYPSi&*gcfg&Tn*Z|6kpi{w=^+#ZV z)b{65OP=)IR|s`qF(>62_Cj5?LwRzMIu@)+0!T#WUaVM^OKthHe)64NEl)!%cB1=F z=mP}(K{Y`&a6nrPF{D&>E{@=7$1f8FQYOHwQV&$rP1TKLm}oPZ;qv~pZV<^F7{^HQ z<^Wpp5OHxt)u8q-UH+iZ`idVe$|NqF%FXnKR)&P?cs zKKe|o~Qc3mHOn^bDKu?wvq`avdLGo8;_Cz{A~5-KG~!RomO< zLG&v$!MIVY){PcRbM_Yp176wr+x~V{2&`cR?`INp(WVN&xyTe9lh(N$o7t)MRppeF+pL3r-G1hK!7NQInfQrHY?Zv`Ku*+Vc zt437)Knldg42!iJUI>NTDi9$q!zstg+;7lf!;%vxQ;+SN9>~E&yg*ZPRNzydoo9&& z(Zm&Ji^Fl<*!4BcfM+B----@ zxvhO%)C+%9g$X7iZqTqlu;YIHCGOKNA4l5H36Gs@n5ys1`QH54o_wXwDirE{@v=?a zas3ZA4K9=3LcT2eCQoKK;S;%Mi=1#ff6#7)wlFd7kRxI~ZU20KYbg7)3%?4__uW`@ zEucKq(o^-iCvI_DzOMO#T_3>k`zaSmqq=aPVP>D1{;C4`=P;Xy`3 z$CI)M-a}JYM9a}?ipoZS?K1z>1nt!&(^#KHbA?u!)8SmI+y_46VM`LhZTqZ~mH`S= zGM-d!>et~vOC*xJjCxb|bP%ZI{eXp(s^I{efXon$ttN;r;gQFt!HZ46e4EtXI_Hv> z^~YOf=?0PiJIv)`_YcZi836B?amUE7cb>b)Fx~vYp0M9{`DZ2c=XR52fjfE}y;>WMd+$#d~;?zH9tB&`!+ZQV(icl|pR$-t-At`oxxe$EPG< zmLrj>e(70`UMJ(r2_le{yW}@tgFsmlAPM+4Y1KO2889ElXedr;m7?!ehBR`lR98S- zj+HTQoGA5P*qTpU)vSVt6S*%k)1sBb#iQyIV~3zgzzjV(}wOs5|?;&(wK59nsBP z+GY7)jsDN5gxm)tc=sJYm?cHK$7cpg*ZrQ>@aCZBG2*JS?$lBCyT%<~=1&O?*nbx; z+1AwAEzgT=84L^8_^G%Yi`Pl_dElXo^!J`xIF3qOC^;BqI z`m%JM5xHoq{?F>tO;IaPeHH_KH(5r3w7 zh3p{)?7*YxXFZ1M#YV%FBVyqz`qwspIN|qUM02I;mBsTQ#Nmmt6NTQJ>>XkovhJsd z(9(0vI2G(UpX8|F3zfYgI?7MqkS~U2UXM%Z^*R{DVS3AohtLcUDq6bZU| z@uK);Zy;RVbTVID)Afr;vg@k2DY{YV()6T0Drr8)l4;(09%oA`8i4D392`rcG2@)C zz&2h|sfAicknX5w$IXH$*c#4973iU`8WZvE7pJe-;JK;POB3?;^vHMRq2^m;nGK5+ z+iE}aIpdF`4=tRf+;piPi6~x&E&rg`>nExKcXsi(Kf3X0fB`c#asg29YzEV#OdAc1NK!2 zVzK=I!)(!eDdB^%O(y%x`N7yvo83UmnoP*7WzM0yJ6#-x*L0#%n#sSDc8ie})(YDN;LL%3(xTz?9l5+`F&{&pPO%0;a+84+lQL zS4OH+;u4}=K}sa2PN6+w04DyLl@Z<7y3ytzL&tdrn|&%`_HY8LM`P(^BkZdCe=6Sb zDV5Z%iw)|s{T~!v!+0}doM`^5JO zLT)si>w9~+j@my*m+1c4(|_EjJO-A{Sn2~ce|s&+bO`{(=yh$&V2tM^!cO!<+pJxQN+aH5IfAI^Q-#yp-k^~ZY`W)KuLz(XRJq{-TS2~|{ zH6BwX3D&4L4~KY>{1Mp0g~^%OEZ75>Zrr!G|B?7=aVg^jEOZ@Id{#``nf$L5UHXxglRQ^j{KhSrY(qlMkvqAF8V}@^}2!_?$e3*l7VDH z<&?}Ra&7&yFs9{pjS&4Bl)8*{9LsU+S*u|~S%XeA-l7sne{o%ksK5)CrT`DbUr0rT zg&+hUE{ihVH4O;Y#}a9i)bDYc~j-tnNiS@Dg8x@$9ji)j+*X+O$i zfgKb8ixXBH-w?L!w{BswNFQe8$zQH3IKuRQ9q@4WrH>??GKian9&rfGD=?zybC>af z9ydBYf)Kl5`M1aJVmkZm$H|hI%ZoU;ri$JR8@Jww?D1~i`ssbpoF>nnjUWc`O6{W; z1m4`&<8vx7rqvbfKl*UQ}@yGh3yMVggTRf`@`` zYpTAw;pH^`(IX`55{s*jAwn6%fIv+i3BFxQQnH#Gq|3 zV=nhb6Kl+;^!oI#c6G45KltK}t+Mc24>aXPFnc_W;EZBav1s$yGobPN1pl&A;4XcS zi&Qp<^E#mt4`tM8fMWnXd$=e~=bdR{&cVz~rI4$BKOU);=94d_`sGPjl^y5ExvfQo z2ddZjjDv37rLUTNUCfT;^~={AkDg$ zqx^jfb*eygb6|=)dvMJYIi~2_;vY`U*A0g|AL0K(G(0y;=N*NJ%ix*q+FRtO$+O4o z6?oh;;fbz0`7FO_(oo&ty422Uc2t%v2vd_re3*T)-Q1QkGqkHKmzPggldn%4P*HVF zO2Urz?3)PP@`z>DZ4T@nZ|0{)uS{NlYaW}7LP@cQm$_`$f@0v_EsTD6-yQvvX21Py zN^u43E6j-GV619bWUidOqr;=%=0ei-CaMNTg>Pm&;u17!(5Rb_wEVkOP#o6`C6r=Yt=B$SGPy}RW+1r6J zyziEJ&j#sZ4Nf9`Xe`EH{UBYzaAb}MYj}-Ra}3RKK&8~?)ma0L!tWebKb4bcyE~Fq z#`NJ*DGp6NlY|u(3L2b0pVw*_2dt6o=Dn*eE%4MC4;k+mC`|CNFzJ`Zc1UU(8iXbo z5mR)d%Roc?VN&z?Rm2wVl+Jum7k^zWqEXG#5Us~D+M_&_?D`hcC}9d$3{lyo3Zwde z_YO_9DE}ZpDeV@6B5Ze8U1l(t9MI;#$I3k+ZzSd=)X_rzP8n1-S=c~(I?GfMMp|%} zsaI2f>5Z~{ekN=<{J$P#_K*`O))A1wq#z(!WUn_LV^YH~(DF8_v^j z{GQqY;Ujif?N)-1EZ!bz*1p$S{9hNsD;>Ug6@BKxd`OmSWjqM|U@lpI{Ci$KSx`HqT^`bTPuH1%e zU%gVX!#u7Cu0m!`bL1Q4{g>7PUp4VX{+~iwkPg8$Pjk_&-|#y)`n__^)RCKZzKfR7 z8=w^1fQw;#+Qie$?o|I7QeG!HYt$Ad4iIPduy4nyWoAkM_Fwo@2)Vl81E~>Jl${FK zm}#>4J_F=DQW1OB_t!iO=>Cp6x%4pWR;O?a9;$%QBvNNtS+)WOxFWthjM-lY9nj-% z+*h?~=hF z!?xsbQsz>uVbt1VCd2&CQr&thr4U`n$I~CBKf{OTy9EP`u;N7FHgI_TZE0c7BJyRSP>DI6KI zmR$MA$~$`)VmsG4PsItZ~LMk1oXnpvcI9AB@l?@|J+Xu$#u!w96;C=0<=2tga&m8Vy$0%?dgsq3U4>BWo#ev? zU&+l)2cLu@VJKDQ&i1l2|8R#@z5Uk_f!Kc4%iiO(*ytTJP>;ZErFfUvBD+^o^$x`D z+&pMweWK6{Rn8c{oz+AmON7}{Mb`U#l3obE)qCI$YE0wrS6m$le!eIzfj=i26NQ@6 z8t$^nDQzXd=0W4rCDFfPYW=_xzBAa^d)y=*9BfXnw_f_j$}HhUB2H4 zsa;IiG>Ea9eegZ^>vh~gG6l)RK20iq9|a5sQO7%__#&ZQoS;hnvX&~o0C@giCfXbH z8(;RWb68WLPrP=z3xDQM);>1M5e-IMXvCymS&699sFK#8-$MG>s2CC4cX1FguJ96^ zj94A9FB47^39{E*(F$_afQ1TxVKg~w&EtETK@cl1+EGGZA! z;^|#kFamf+tEs;g!K;?nm>ne=RFu-QqB>qu8s&25Um8#JX7OYcNX&~PD(FW{_>XHIKrv=Bla1tfm+p$0f}YO zNu*evzuC9u>*v0Mx#1O3xU7wOZ zQNNkoQupv-=y~^{zwRQeE&oP`Qw}FeH4i|`H*Fi~O6C_F&SLIX)3-?1;xba9vLXrB1H9d4N zbU|4*CboRjq444&!X{@_$>Zr$abj5Y`Vi0Tp5CGU(s_rh@b4U~ueO#HJl|pcFoY3M zW8_SszK5m3ICH^jE#)ckub<5vp2^;l&ozHX82@@L)WD%7sV*e5vr0S?bVUvc!`)Q4#NrA0L}ZAvqk~1AArqFY)i{ zw0pq49K^}mks2dtBz(qi=RPwRjD}(8{<3++%KG|6Mu79q^v@4M(h79Zd$8x@JuA(N(*6GIy3@sM z+mrl|H8Io4`9S9x>o5Nnymd|vd&TE&H6g!8g=^8LZvq$o`0xB>bQ`fX&5K#`?=_;` z25xxP1>S9oPAASEyD3H=Mk%6i_}^#2&AxL_MO;9)hKn_^5Jp^Rczei<3;LEKW{aW} zTIxW2Ho?tH^ooCAHZG`}^Ye(HCaNK5&?^pHm&eniAw{92BY^xficF`Ll8y+hh zh(T8rJBTbJ%Bp!kCezx+qTvStJ(jWLNll+_Ps)@Zq0n9H$2tDHFK(VPM1GG88d)Wg zO#YD3d5tDSEPEirmx4mAuu@4Wb|gFsNlOBvUJ?3W6$x(_&-SCV#JSLk7DuIf!i|Dv z3<6Vjf`%;`LqY5wJiUq`;$-#`Vg7bHgya5eA00@l$YQ#LOYc63B%RvO9`hmsJyfd_ z+7De!pK~@sxwczk52JHl8pbGsImth}Rt+R)YqQ6Dd&);1-|91WGj=keicXFcUP!#b zD16UO@O*^3ctq+?{xc)QW3fLZK1F+teq{d&{j#LBJGYz(-JYNCrzbF9s?$0`ypm82 zIaik*JSN=YU9Wlqz%m?DE0?6Rt}oB$dm$GW(mt0WQmaM+`BzMMYZFiILe;UQsd(T} zogxCKbPF4T&UPfJ80$3f$hQzK>c<%}RP?n!_nWpfWg?aY zTM`xZ+`%*_+ZeM1S7gn~9Z9RCTyIQXbJgBBU9KH&FKTm+T(me1lp)Dr(TTR&r$PEy z0uIxdv-^S@g#wsa;Z_#awxW|-#P=VO@k4H#m)_K0+1XJS2X|3IGAwUjX+pC0T9^=8 zFqXE1YFQ2Zl}d6(%L`LKJrgRGBcR&Um{yw(M<3~Sne_Rk`;H?$rG@jyoV6EX%?!C% zP-BK?dHfZJjWCRzu!_6Yzl-`D(XYm?yEZlJD0K9j06}-aoc$Iiil>_g#n;!Dfc@qQ z^?y|Ox#DRe(0YB`KC*qr{iE=i^qTW|&njk1`-d=>xqH+3R1X?Hag9qQQyMNFj}Taz zwR(!Lz4jsNqBTxTRUfB+P^^oFHt62SQ-sVAA^@&sd>?90e%e^b9NsjH zdAlY#Y}*XT^3a%cl_2`P#p=WGeyYw6<|{m_PxfUy+o~9KSNf0i<)`?6ARVOXGCUQ%>ZU4g2wup&*affcGiLkZuX6ztZehNXG)Wdp2&&u8Q2mqbibieM_+6Dhoo&Q*Hzh~9_Rnt z=^&S^&*6>PYoVmM$p|S-;}w#LtQ_exakr7HDlb3ZHX2twuYo)K!LCtgrlf&jcE(TDgxH{x`zx6WaBNcvI5wXc*>SrB-alHTHwt63Av)~?=zvRUNbCTU zZAp+PL}GI<)_(B%5cJyy6%&(+Mij zHAOiE)VfEs5%nnK48MW8zrGb_XX7NNoqNO26mqEp1TvMc>Kb^8-*)^2GnJD;CRZ*w zWjhTeGrfBJ=)l!^LzY4{g z_;2S*SwACL=~Yx%PBSCOQcfqs;ub3Bz}#i#^^s%#Dg|^6sKN3XK9xut%!A~1W5h7O z9D2Cru&XZbpB|GLAMsax1M5C){r7SijAs7>HQIYmx@sdPJ5Rl`mYt^S2SFK=Jqi#| z<=VHjZl|7U_hZ+_*KL^uMOV0Qu+dqLoS+Qonqc;c6+IS z=a#2o)uuD;tW)1L6tGqadOeQ|8>#AW!irW?+CN$I*FPhLAS z&==96SwVj{o>>)ro?gI^!wwbt#Gsnr#pRl`-G=uyZBdS)7f}Agjiu$R&qa4GLjr}0 zjwNGZ3OCpY1g6)o4#6rR-(*2#prsmg2pZaXrrDM&*1KgK5AqjH7uE$$-fBjzdUuZjO<28{u0cBTZ(4f3i^Wb~x4AH%h=RZ!qV9^) z75;AS`K2$~=u}Zj!B)TcnX!C;7PJ`iHXHb7<#LzVE(*8q$emts!9P?$398LcG!;aNqZ(0wGp17okx7Ea(R2-GrflCeO#96e%$IfbN$7;|p|LMyzbANo2qwo%!r@>Ckw86>D z|DtpuP*`IjpT%76f_%Qdpj6XYH;z?(SRCUm_>qbz$J?% zhoo-?QbyODiZ;JP4Bgwl>o$3zluQUsIjsANg7WkTI`tn9V>-&;< zod&YfWx?0e#W23_S5;$%3h-TyV>}yfujXc=!$Cg{g#J+q$tQ6??D!7;g%#bYb$Fd! z12mxLoCjIO=yhJwvUCF9=Aa+Y!G4SnH@<0V2^~8jhIJ*Fj)>PhTBw%vW%Wb!-d!|W z=t}Wwvc$$ellW%eA--eZpFFz@_DJ#7-LiCHbEI*8!=KxFOtkp;uA2xkke(@UG*AUH zU20Dg^N9qL|4M8@8)$wBx3aNa(1)GwR{fqDv$v5uCG8PI)7_g%qq4w8PNQSV89W{$ zGaJuc%VNq_P7(5o6uT${W1{_4|4R62BL;vjaKYFgx8ti!UP4^V3Z;lNN9)lT;F=x#~eGA86e9JrM=K z1Q3%xkRyPVKtQC&z@Wg0|K(Y%q2{vgJ-KXCvH3FOP~m&u0&u;k&?saC=on>5&I@~A zG%Ol|ithP0Z%9Is`b0j^#jsU@EM)Afbx1App5_3ziG9yyZe2--EQ=2T+=g6Yw~6`V zE{ab=Dv9xn-48GYE)xpUFalULQBtau3emk39lD65LX$M6={%ePqJZU@nm$}q!c$fA_h zjmoMGq4}k7-}bvmvW|P|G4nn?AVRmqGYRq)JapbKAk|0cuM>yIjw@>2G=alDbk zQBr1C8e?^)z~@V>2vkFYKAbZVTYm zenw3UD!S*rW(x_K|HW4?x-p5++I~+F(x>4^N$QQfUoIpSRqK$ns}x`k$6&bFtq`}sqnF*+&MB0Or+a3^N6B{*5c?*b73ZCG8hyV!L``u8aZ_@WXB0yqX_t|ak@VBo zd9dtjsG8^C9-&PL(Zkp8bJTEyHt@C$aQ8ETXFAf=Y2L#Apr~qd)Xsf#NEn^8H%8az z#9IR4)DPRVdJ8_`M;uo!JGPs=m97dQhMX~2!|~9xb19@IW6)zlousYKoGQjuEBCcO zEo9W`>7&D?I1P(c+S}eufGVvYv94-5kpTH9)skp)Gn`9a)V{vQ(R9I3Y2ko~yGnkn zQ4fbzha#B^+h4Ol0uw6!8KraPYxLM(KHCbrigpM2X@n^$N*ZaHG-8OI{<~6nCn!Xp z-4_Ye5o%M#qSf@Kmm)vkQFR8I{@<)b0vj4FW*@qY%WP-ijzU2uv$MbRmS32r$ zJr%^a%OhZM_!50+$cC3JL!D2@-NT4CiFWVDl{c@DjOuO$SSnt7b_km7)TznK+lhk& z_%?;N6M8V&e6+PAyc+wxYGFPJ$iOX7#>qkibvI;p1m;96G+D~^6L9t<*cGIW@G7uN zO*D_v5F%MBB8=%essD{>ZJ{@vveME3(&#z5syi~kK>jLFq{S|pW!OnmOiG=pKqkp| zlUj6L5IFIrtxa5lO)H`q6kBbqxBIf_`q*gV>6uz|RjgaN^<7|qpNaG6Bq*@a?z#nQ zxO|yUT3cU#Zz@-^*hR6;Y7=dZOR3XMWzjW9MlRCDR#%_;yLx)|%4*P+hZH`g(kwcLyTitun^?xq%?0wCRim z9@*?MGQa3F;Nl&!5G`)cMagcuT?V(^B`@wGC1>N?b>;*4nuwf)<$o{zb z@suXZeOo-s^Zq(Q9m}4y!m;PzLXbUJce_7y1iPU4xvJ<`S9RMl2t{!sys`Gg~@34r_z&2)lN6Z?5b2uWaTO8`jR z>%6hho8{W55>V)R&ks?Kd;F{VLjL{&+mt{JhVU)W+ti zJn4|%4^1`7S{5-O-0LrTl$_%GqJg8&kx%Jx^_tIpQd>q($KT8Q!#@l zvc@X9>p|lJn7Xd&aGXUbIuktrkv~NGNx9_kZV`Toh#6-;mc8!dT z6WZ;EvoD^D51pCb_ivA?52=H68U3F~L|I<;9B$4p-ek^~KU{{sKtb7(!L#`9kpA-v zLUJdwZgDU9wpnjNBOt?4vxN;}hU+xBxqZBT`x)v)#P6^Qp4VOqul%}&4jtsZoMyB6 z+z;gWSQ|;78gyF04A{s;m%h@fj-yh+d1_+8eE2#dxi8)!VRcUQ3lmm3G0Y#aKmb~?4w5^FQV$mNYRm4Xqz4jk3r zhDX}r(*+Dwg=ecnhT&9J#Kl+6FD!)-TD2WobxJF8!--?gCo#EPHgPxLxu_-2G8}FJ)MT zHm4I@EiR`j20QO*>bNAgN{0NM9fKQ_D_+X0K~FV*0D(bbG1YaUg*G9ICz)W^WMOaq zxZnBQFRAauh4%O7q}}**ix+y@Ql+;gNt7!8Vn6*3yT%Rk(M*Pm7!77kgA4OzU&aH) zpQS|vILWvnH}^Id!X8xgKm`|R(h2^R#&K#}QVdyyT(bOAy#@L~IgPJZEe1M05gj5U z*VK{-9ktcv{t6@KM+{x?1lq;w3e-v-Agjzj$dHR%)FJlF|X1XtYj3ZaA`eMU#ZMnX^ZmkdtdLZJuiSU{CdnfewN-Dzca3V!hGfPHt%+Xo(dC2r z7;4az_DekfkEyo|YjbPbK+yuFxE3v5+}+)wxVyW%7q{ZBrC4zYZo%E5xLcsOTOfzM z_xqi5*3ab0bLE;fa?ibH%@F;;_93&~5e6hWNa6bmL-NexpbK8-WO-vs4Sm-f$hybw z(n{C=R6J%8uubZePvYh<_L??tmt$Zc&JM}jYOXZgTLy4xP`>iaV*8ke6U(x%q^~6~ zzPLyQr&r4LR`nQ8s1o1krZ3S%NJvEEo6!AupYqq~$MVPWuk4>>cBGi{MAL!0F|p?* zW;lRy$2!mH;LgV9E8M!Gf0n7)a`O|s`xQHbB50gYO6cl)!a1KLfG_a9c^xG5A#iJQ zy#?}+`o5DvyiZuGJWAtUL6u0r z?~(D3Icr`4*G`Nu2~CLIoXyn;YALyoE~M2XxT8(>$tMT%8o8F}NsL5YEFTnDyBzXN za9Lbk8146t{@V4X`exL!4@uP#dZS&6ZiiO18-6euyduY8o0h3;wqf(Jq-!YV&0$K{ zi}^j*j_uOd!^K})>h{{w&dz2dbCf0=yMc3f1BHCd&G0cKD}0$3kor9IOST&Py(&z8 zmhJg^dGp24LsQ+E)l!ThB|Z7(z6wd=r46@8DNNze$F5V6Pnvr54queU#~@WV$y9-Y#Al5JhV)BVr>?9PVC3tvfY-sE1Cx{z8x#uO+U%x==sPh1H0ESA8~q=EOt z%e+r;FX^N!Oa;U_PKT%W9X>lWB=vM{cRN98;rERZ*Pj`ZeBNo5D>wU>V=W z4&z*Y%HA;ZslWel+mtcy_hH17^NmB}?R2(Sjrm!dXtad#HtUv>UW|y`js%@qapoC- zx{cmTwS$yT6=qt%hBIt(8r#mokL(NQXK3wJ=|kBe1@)n<4ek%)8?=;G4QCp_Pc&hM>mLh*<*Y7XpSQG1OC&1&st!?xbV|>nJ?XNyq5p2 zVU7PAdzMlVR#I2oethTt>U^Jx$2-K*!G)?)&PH*e+;yVx)L%3D&>l;1!Oea|^t%2I zx`*z?c8On?e8#h2u{ih9+Gi8$H!j3}3r(So}@+TRpfPpJLYympC25JrSvA zfDOam@%x|;XLs_jR`!aGf+SuWQ=ghXPlhSy>qmhcAfA&XesW2*&-r6IKC)=~hrQJT zF=~Ryn5MukxrjWBm{Q;Q^<_JW{FG!e{Hwq>zVYmg6S_sCkQGkL?+jG*=wwC-F|?d| zOYal6#-ik?VL}bcc&yw;j4(?T4oh+ozJ`M>q)yY8O3{M>0wbmeO&wjoaX2&55t%9x zB;n4+W#Zr%Yo8Jq7zfBIC?^?*i`>cB2pG}#QXTnxMt~>(WZnc0HhNr74bf>DW&VcqBmWk$Xf=*(A27ex=ui;C$OqJ78Fvia+78EWD(G0r&r7q3 zo}4|f@6W_sNwaesX||uV?VZ>rBL`?6RL!W3-L1V%A%&MQP}ZB8A+x&z_9@ai^%cM0 z7Z=`yrQ&nuJLCw)##*tGRHu1=``2((Tvq!Qtd)0Or#s@h#)0GcC9#X@D1!Rl6Qy~% zU61me``Il8;1n$)q%th%@?fU%=M}q3c9;ZOLT6Z=_q!E5LTgUETiI5+kzl{_#jP)= zTho*^_44m>?~hY>dquirmtL{>On>o+Zh6wRV?J5EyL?p@ThL=?_~!4a?+mT%e~L-~ zQ=u5dPhbgv4rN2L`U|8eUy9MKk}q8j6`e)6m1D=~eJavavpYRwe_J;(!)us2+;_!@ z+Ro+X)QbtQktl*4#kKGlka0r$!Ad}z6!eCCy@&#KyqUblGnp~TVSu?J9EQhv^w9}x+3aRN?7(z(#qQ#JU02i#Ym$o(ZHaKptZUWe5!df>lLSZyRIixLBi#~>{om{*4=*x zQAV*lS{j=AtMRl@mC~(6yVxQU0+(ey%GIm4RF-r=J6AHrL7QM)?~sBPU0epA$?e^KVSYL_mogYHHMFnQkkKMMP!657j!gUE)Nk zDXpT(plzIj)`ze-J(oV0cJ$zb zO2}A~6JZL*m(K*-MGr83#GK!DN*T#XY`ERwPFA0~uiL?Az4`bk!pb>;6(h zMZltgM#+i2unT)^os8K5ZpGKo4=cTRGF>p=K7ghMw4$O7&`!reG91A-`RIHSG0D>p z650yUv*nx`M15mpVOQQ5NeV~NzfSnL999QZs@l!POiD0FhXDa;Yur|XnSfqdbY|jwiro$t(fW)e;Gi*+vt)pYo$s$Hc zCbCB;wb$Ij!>>hrZN>3deV zX2&;BFY7>k^;z%fC;sLKxd8!+xX^4JR|C`nKu6ARWQPlf;NQq7i)<&kX7h(vUzo%# z&9LDM5LqYHJxh1xU%3M-MrE2*(M`pF+Ez$W5P`;pBkpZ!x14^HPfcQkTcA91Cj1T( zPQ#uEjOX>7wtgueE4&p$onFL(m(`)wi~=lx?pCy^5SX{Zi&ND<`&far zvPIjXY{e`M%@;rlU8)ZbW`s2FA_^2@nu(IEMqLQn4y-vQG0xxVG5$RRQ?f2b7 z#+Le@uO6BV$zmg#ddg#Ra*DrcBs7|YBl*bO$x?olZo2K-E6vuFU6oRQs;3x0r6DP* zPN9|=-NeH~_B2{e(ug35EVedcz!hVoR-fAzh`;(S=wrWcc#5nJ#cfomx=>j1nTHC% zQHFn2;Xa26eC93fIN$q@XAtvw=|2LJj*hKLr1%4T!A~AW#GX&^eQITfZ;vkL%t0q7 z&$}489t;Si>{l5HDRl8e<+-+1hIQv88-3zH*j42&P0~#s6IaSlnZC^r@*jt5R7)1gLhhc?D#t1Gu)W?e@Y| z=Gn5`2aIdiY@00OehP1bK1qz|gu|-3c?N__C{Shr1v7a4F8u5s|9UVivE)SR3=yjM zLj~QSoIafhi_VAPknWTPCCB{Vl15l`YZOW@6!G5;K;SxCsrPs&;~wUA*bjc2?3yB9 z;C63#%Z$(4^Bx6u&ad9A1-D;VfBhTRRn*eClY88e-&~b5f@^F*|Lxy&-PFvw@M%}L z&iO_wQFfRuMhZ~C6(6uwS}9W?#xSTYM^UWhiZTi$)5$1`UaR8G%vH1+_+wkL!(gX; zIHgSe@VwqBdShQdNQbUQ{xb|aT0F-^LDO0FkVb4lVqxcrXi-GI_E2wQj$a@7LU5{M zGlqXc;#S86lv}|0$*#B@*T8Wjk4DWcM4EbH%mW2!`p;x9d8rV?3!W+?n)OqAw#ZRh z0P-IE(B4%~QFWJO$3%;RUUq3Ow-3|gC0~pNUQDvuz?h(1zjG~lswh&7%oJj3zCYs< zM`z&Zn5Y+|>ojPbaK-lsdWc@Foq0+1>i?A@mJ$v87ZLFKl%J#jpaFm2QILSmZT?Mn z!*!$8z+q$r|EILTI^DAtq3I@-_qL?^G~dBF@Xe%Mw`LS@Te`za68Eo*>93H$&V#Dg zgbs&eqAx^uVgI_BVK^Pc##pn)lc@TB1W_{yS6IeR-KnPepWLTLh%PpA8UM)9Sq@?c zlp3iHbaX%diqk`^{`8u<8ly~k}!$_zzmp*kzc{hJp+cD%UecjSZOZImC_T{ zl$wd)^M!|fF)iM$8ZZ|L!^I)I4gb<_N6Q<`)hu2-YG(nbCCA=Zw-1lYnTx_&8+Ho7 zWi8PsBQiMLQ%@VI`rQ^svod!&`1y(s18y~mA|eVC!B!Yrc10>L2;th7ESj3%nNwEz zD9!FqZtGE(L4sE@u966|csn`=KbwL4+EJBv?-mGeg?) zJz75mzdrX|#F29ChAsYIEdcnA+1| z3E3v=$<2&Las0-2GY+bpHhVQDmu~|m_d5zhI8?cq)y3k68t#&>nyJBn)7Fqbo{4DM zGNbrrnpsrn{1AmKuAIt>Y(+dO+rRR193{LZCcultQ*6Lv1*`Xfz>uw^d0Y}cOmZB@ z0xvzl;$wF8;=N+Mb}4-C9xF_3s@((|M*N?f&AaJI`UC6>#)4YyR0B=un(l}J`}n+I z>mm$miRDHX)1MA5Uk!U=*{(^Xzhjf>ZvOg;Nb)C3uzfk-?~j=#MSHKlRWZraL)aT= zr`|%aqsELaYtBQh=ps=i_K`JeuH>92Xy^YtCY=&ZE*IQ3)YX(%S5Y&Gbgw=3c2+F1`^seZBGw^x5Z=ah5#QT$ogBWH+0=_)_pA8<8rnq%$6 zsF8fe3}vHq#XS)M>#JIeu3I@O@?|X{)}xi<6Cs(-3|{E|916u%F%9~7SfmBjLF=0? z?fy>;>s=zc{_n_oIi)uE@lrgjyOl11IQ4&GQ2`qGYS45#B~}mW1+`w51@T`z;{{h% zfR0R7_@xTi=@Y=jQ5{6qY!p6voKc@}^3n-$EG2BRS*{=9E}>rc?ArCxdxDezWjj^V z1^a|jBVh`P(VMugyCvPg(F)n;ldpe=cdGq8rp)&U0mOD@ZBev^+17S2&i^Gf?^OB6 zvNU*Y4GoF8;gzQAppCcDpojMvEx`NJa7De%j^k}B2-}9&LuHH569gQBkg>Ap(DT<0 z7KTs_d~oAr4KV|J5Ej@s!hYOcOmA^|;0}7OS$li21OFH{00#$c3ex*Qj;_~gU;XHw z{z33AVo5~B=n9f}zc9Ee!dMtASQag;Mbqq=sj&1*Mi&nzIy1>@8s84j5kX)gR&Mox zYNm<0o2heL#rcWrbJr}mM39~gFLxOXLv@3=tIaY`r>@CSZjKuH{ZqJ2>HAbt%-=1f z4+lh3Np%s1E-)^om<;#Uk$|IE1t#kFtXuwE<1>f?HA-U>v)M}X%%I^bsEha{58+T= z!uOzfnah-#IB#-TrZ?&fhoXGc>@@*0#@fUfzZ{@8UaxBE=I>;pYGA?kVNJgwX$H=( zL=MqxpkeTk&bi6h{Xwx@=yoSJj@ADp`!3oKWjN4?u$aylo8ntt58{xwr71|MlkZge zneZ&fFDs!sgF9jn^T>JWEts1}n9#49DM4V3834`X8hsoPbg_8_y;g(PtZ9y`}JS>?k3^5#y==fL#qtNvJb8W zQ-(fy>QI1t`~Y64z#P_aqejarS64{^2RyQazt((cYn6!At5*p(wM2SB}ESC)Kn@(gx&9pL# z_AM;&^_>8{cb@@NFP)Ce z0a0)6uZq5LXxO|7CO*Mn=lr^@^WsBG09*BLU3uj@J(52m->2~pw!Y$2*I!nu8s=Un& z4YR-)tLpi60#m%q{WcXNXSt+@sQ8j)$V&Gq=36F)?;ON&=6zyoH#82m+S6np-bBTE zO0?;X>)Z^Hnfd=HjrUk`96z576~>F{jC9!pyVM1VF?!u8@ZZQ8LeAl$xmiz~0slP2 z2S6u;u;kLhEp7Tx$cY(55()f8f5J}!@5pcW8ld^_H|ID$!qgE`K=}SM@~b)#f*H7K zC!{$H)nXOaIg3Ibdd)>0H3sectl3HYIhZ^2eENQH&yDWc< zc&WAIZubkmgnh=chPZi>Xh^0M@~F$})vu_TA(k^!;YvnpE(CosFELZ#0VNGhs=IZ? zH{obx{_B)ubDBZ6a!?M*C)oH+pnkdDRA5bl3Xcc8Jagr6i zn%h>E72N1&x4Bq(Z~w|+1H7;JNW0Uy64v#wLXey0O{-&B=!Ga{TQus5dGnH1tjVHU zBT73q+L^E%VCeiMzqqg@YH)HPLbm!*A_d({Ix0j&EUMViwm$~{bR;GET+G;rlR!tmBc$22L9OL3lYvbd^pi(|R5_3@Z+lof9nLF`RrthlY{ z%EWAC$RgC%Gx^rqJE6O#r-##nLBUI}1k&}Dj9cbc)ng`aRBF9jT$4pw;3W|5dx^lO z*i-3u=gR_+_+6-A8Z+%8kI2?#78Q7|lHm z?MWPoN$&~l68%z5u|yB67GoV) zZAKm4yGig^O6E??jzU{5Qwvt6#Sk;T>C3z-&-05{$8wNi@I<{JmJgI$TKLrS{{yym zFf0_NW6k0{=ari`i^4&Fm<4y>*P06+%-8(wR{{tJrCh;>Lqei(JFAN(P-O1&=jlP< z^$Qp@)2tx1344vve;q<3vQ-c))`d34aGUTIAGLB@RH>S3T$k-cbkj(7ROL6`=u+B} zFZrnp>L!j-f2*19pw#?#EQ_(;cO#wAYK-0P)*z46DqZ!=;WrcgRw^7)J~&>PoNDw? z=j^N&yJ@q>)jmdI31Z*(qCfL9>x%?!h8<;DgU5-bdNuJ%w0O#E)>zwe_HY<}%5PFy z^DG>WdVmQR3E}6o0FF21bOTXS1HC zz{@((PT^m|-`gdgDEy|Kike{~jrs1D7XOq^uf zSkWyRh!Gj`@ZXs}h&d zqxZgEH}b%IBT2z};VYNbu}Z3rWB|yyX4E`4O`}!Rvrw$Ij<9)zI*aobmu-rBbEHmN zJ%|hVWG_mwU|*F*P-l)@WEsoYXl!|BFMuOqWnO?4NvLfTNtr8R^y7Q0;RaWSBL%qz zrjkKHp4Ron`ixlB6WJFy|L(T--0(~eavs1M3`xC@BtZ4S-R@IX55UeXg4Lbl~_!n!SMbup4(dj5Eaz3;c|oYJX!m{tf8ET*m)Hz zxC95EzC{}vb@S~YsB@>=N)QCA?0r3NNIOLuII#^$p!!@#)I#>BK+#crf6I5Tx)waI z3hABe7VmhwuMT=9>@ifp;q6sV{x|1LPd{9wS{ajEzHaII}P;h6eup4R~+`060VFrJnwK06GW73NjH?37QwpY{x!>+ zfy)MKkPI=N!&-r36H!9AwQPtd2X9~O6OBY3ExwwZx8;)I;y`m~)JB%6GUd9x(|0Rfk^AO9ft;=L#~u=4*Dmu?E3DD?acBO&TH6dLr)76b&j@j^RY zaqAdwlSp=RWvC$}k#oKR6nP0EQ z#>QZMh&8X<7f%245TD0UP2C452e2oK2#Ja1usNFyn>Tzz5;0X2`|@3*CFEkH2BUFg z@P%;!aue8LmSs5fNis-QO+?<%vti|eOHngXzwSt8{kPomRi~iE=bTGra+}uE{nu2D zXSEk|Z8cF21q7>d`~8&PD=*{Au6_*hcNUx>%y?&9*x))H^&Hkp1+H>n#v=+{fDq6! zfZBH15k=Vx>mm$v@9fLd-4Y_@&lWJBi>KhOGo7~!Z>%y~FD`~kZlq?R4bc>BH-CoEGT;LuozO zdA`!bSd#$@FAui|Ha9O2?`l$u|J;?wo4!rtM z0t6lk3!fkuf(wTC!dZLTT{|ff9D{2~WVP&ZO-}4GkjF{=5?zp{MH=1z z>1}#+O7K2YFPOlDz$_b}mT|`>rlFbF@fFqK&0=!=E()>Q8(S}ODC?m8Uj+OQ2%ZgP zKinutcc!A~dIuB`=d<0VGS8CL%gUBTq5<9&>gu9}MwVcGgBf)d>WPbx%wyu8&@?Uf#(~Id)S3>@m*1BGfPH#=+b)>glI|+}L z345{sn$5f#VI|=dT(Oc`2$mtY{i>i?d+51&TD*zG`e8f*t9R~_JUSRF;UWG2$r495 z3TZG&JxbI~zUFq2Sn3|yTkVc>ZtH|&OI-q!*3+8$S%#QaDV}8lE*~>)z;589k`&5Z z=0=N)rZL8^CVV?N%~mo=AFBcFPtnVgYVB>NDe;I%9NA*Zm^G}i4B5WB^$r3KW^&ET zRHonY!&8Z9+n}?)QpUftm9*rhW^^$(jNPAgY^11-)MARUU-W=(C5)}2b$9pO@M-$n zrgrgsJ)yn;nG|*`46&$z@-QfaZ>C|XB2A}zA?Xt0s*zsR;c?y_KV107tYYFiS+0M~ zmLD$_ItV)K;kVqsy!R1#N11))7VBL1eoMaSb}mw33*MV|^~yI{wd*l)(~T|vm0tg2 z+jcu~bx?Sps(O&df&7gP4c9%@-2P7q`A=0BV4N+IrwzyYa{?|VoZN#ed-pAmC?H#c z*%ye&O2^9^q44uk&^@qjam%9Wt_3&f--|R7>mHDgL3ih9Grwn^;c@Zw2rgz5aZK=E zX*j-;C?+k?&oq$z;pDlJN-xP|tSKy#!sC^m6NwR%TQszLKoy-;Xh=d5JZ9{b9jy?a z9K@TAl5R-jXg>4PO_ZB_=xfO6n{J&R$q(~6LXR8lL#ekl=f;h^<+ugs`Ak1 zlZaN=*KeowP^e2-?uIcXnuuh#a)LD9BP7Z!l|46q_JjNMiD0A_WHm9d87ugqh=d$CSR&Ft&=gyDw)-$sB_aIc~rw3S9PqvSRSYA%bB+I*`6>W z;hZ*U63QoIW|`*Nk`LOypwhN5Ps=HeNVuv9f30dLSG3y{HTHR-32p2#|ECB6u zgyGpz+JoNqOpZE7gf zxt1t6;+QR#@tyE-e#4s8FAAYlCqQMAG?laFkjxvYsuR+AjqP1A76Q|rEw2N2Tb;j(a8WKg^WcNrK4E4K` zAE&&qQR|^mR?q||b0kSb!6kcI+iLT?z?7aLs#xdPYk0!QYU>^!U4F$VidU~U^IK4l*J0TvEjbVt9Fy*u^=}57@?D;}>-KZcG|UzSx$?I^j8koz8hI5;66POI zRsXC>?q?4U4tjOpRXcSZU@LdLNAuk{3aFnn(2?O^5FSq3FY^5ypH__wZYQ^%r}H?5 z)sfhw!Ni`$4Hvx#+J<}*`dKrIS`+7CyEze8yVTjUnbn|EV-8)OzGXr(h{9-{FHro& zbL**_`*rrhWO0j%;iXRZKaLz)vmN&FgQ;_(fYak^OEMg|+(Lz>^DqsP>hzLc_9a_J zG|@8rmGlYys$Xl{OeBTe6jz-}%$+(>4_lw6>eQPJ@2zjTKAHM)Uu7wn&$0E@@a=L^ z%h|f9wL5O{oN<#gfTFfIB0Us*YG*VwINgevk*;GY!LDPv#Si)P@UWD5pgz%T*KOi# zCQ2B&9X&77dSsJ(1At`Lti;5ww#)|}#u|Z$FWwLzF-DK`};vdz<&&QwJQqRU6PO>79zDJe?@~;K%HYw@w1R1mD{p%H63_M#iQtrt7B_&a_xBDv0KgH ze;!L|V7VfR3)9-%v{r|wk64Vp!fj}LlJE-mJPDd_kB}ns$t=>p8=byM(>rJLs{{Pq zcb(?$Xx?6!SFWUKO-F@B->tPF3+_ox#b1nd7R3LIC# zl$UMu&jnKfzRNMK?(07Ez948rI&!_pH92#cx0gfkEj+m^>*d_R!2!3v=ZZz7`W=A$ z8e-@&h(jvS2BVYOtk8JsL`Z~hD2|m;kyeAAaerW>?%f7t`4Z51~0>=m?rWVoo)Lwxr zE5G55Z{LxCer6Gp=77tYmD4_>mi{0|v$bw-ERK%#*yo_BB-e$ZMe0kn+5n`4u5F?m ztC)9PL-oJRV=#I#!Bq+S@qtW%pXf)DTY5Y)zY4hSHqM;l8TmWj=V_UyLqq7=!)Vo3BwBuA?JQ$INz%~YZhf`ukRzt zGTWw@EskQO!k==@Vpn66q#Gs(>*L{jFVLCdutJF?=Y!!IkRWO6kLVxPuAb6+dM95N zqxd9?2Zw3#1Ot(4U-L=L*MpJse`}EdZcxvCP9UcKTW5_nS=gO8VNsavVSq$&|Bt5` z@Xh`ZG{9va9xE~LP=!Jfo5l|F=6gTcn*^`b3u=sL7}~k0FN!5l33wIHoET3^+aI@& zLltXb@|nmpD*rl^nY;dj@vK;wx^Cx)r`lRdQkgJxm{aW(?Q!3SXG{Z68^%$~{}wPX zFo4%uy3QQtu{f7vVx%#rmc4?q-~eNi6vOGkf(0*+kwJb9jh&r`_4{$cmY!P_CJ(Rt z(X8%hDWnr$_&-Rm_)INxZ?0s1~5@Nme6nfrJe$SIf=sZsG!aFmh#G#RsC_7&Q4pAC24fWuVa za6SszIDL9K`K+7~chry35nR5H85gBS)hh9ArxPTY@wz8jINZ5y23GTzlB(Mq-u~#hPepe0;B`)4pTJ;29$&nP-)ZDO!C* zDUn&DfJ8RyxH3>`C6F>goy{cropQ`0=)t1Kd#fyqGV)OK0GnSmo7_DSCK;2}UEsVA z;Qc!N`c+RFh2w`!igj+o!w{j(MJu6~byhJ-eGbbRDn=h%GA9)R3PWsPo0ZF^8!(|} zV@!sa>|;47=E0!fV`*!Qz{l6SNEu0QC z>iYHIbb!lJmsjAv%q#;T)!W!6tJFF0hD%l9rqG|70}Le8>V6RSP95^ew%Jdlp6b4J z#qB=9>H9vi609|8?+76OiKuN4Uxke!>xdhyG~sjFqTsl#EzD^n@VM3&x? z;NoDKS?x9^LmcL9yQ#YrJ2hW%1%B;Pm;)sgG32 zsr5kokX;9khxNh6@B_qn9;WU`8-cUqgkj`Hr5j%}kjUjOJ|`o^1qO=#`?>pU<|XYM z6yuL!fg4jMaMHMrp}q(%H`$yyRT(TuaWJ!bY2!R^{q2yOY`i178Z$>itnG+adAswb zrM~XSTKA_7!O&j)>kppG_-(<)YO9M)!FB&V#kg-$aLxR~)XerTA)va;WK#2X8!RPV za?~yR-X=y4Min@ke9y+H)744hi&rQ;LiVTe? zihKA81gn-ol98X!$ukE4E8cL?ex;~L3{ABK`K?%FocSKh8$F*!o(Q}bn!;GM z!?%7hee#y?>86LAq6LEf0KpLd7oD}~0R8$Wn65uw6b#wa78kJm$EAc?2p4TZGzqUm zWfpf^vs;8|_k9$T6yZ!8cTLQ%qi?Sr7bVSan*RiNQ}pa`BYwztVqhDxu~y)<{>xrB zKgqU0Q2uLYr2sJ24G8gDxl8M41qvIJgctFmJzl$W&mXga@__K3d9aRn0ykIwAg=&= ztstkcd3SxK zcpCnE%LCb};6{uK#q+5~zB>=iA3T4$${7^hfjzYW&C=|!b{63=?tsv?pG-56XE}Ud z_G?K9{v5*ga6#abUtll=ncnn?9Mwn*0^j6q*o(=)z#(=~6si`}3q~!NTcvEP7SqaA zMU5$o%+6qk!b2lV7R#`UH1H$!entGMSn_~61cgMFm|uPK_QvDY;B`JV(Z$ZMhdBlE zIb7*_$oHG#_p)?+dLI+c4g8@(p8$HUVkh0q=}1)JcQ1L-LFT#K-tckieVr({2phgz za%*0gh`d`m^IbY&4Qnw9$k`#L& zHkm8I$$)cFzC^VJM(=hflSH|;DB-&$_JqXZw&PF)?8!yoj7j8K`$aF_(x=Y?HFHkC-wuJVz_RR`?%T>9{dNH;{!5s` z;k_$w0TN{(g$?Ppl5zh?X%Gz7L{;4`@FV%L{&wHUU%ScEaf|yKDD=BWI}X5_pXoa- ztaE2$Z(n8S;1B@2Oxf&m*H+!u_S71PzqEIZqj`e9O%%eCUionyhQSy7rh_(7@Nf&vW+TVBE7j?Q6zgKlhTQXLtdnJ^s?*6y}Ho*O>Ta=|bN7hbZxB%_~(y( z_+)v{QipJR0KT$F==)8#455a8jGnA(o$OTSE2ySshtByA$A7W$-wR zQ|8J>k7BYT7XS!54RbT@?byt@sJ*w&HhJg$frB&+-PpN8PYJ3D9``-F_5i(8R~;>% z^!%#tc0Juq`r2H3a3xUiguSY;-^hOiG8q}aJ?oD+(Kh7eP2{{THu5@kKJ(S3GEEB` zJ@)@Ay96sJ6H6!gRnpcEJY^`$ytggq$j@NeA}AFM*5AD-rLGZ53l6sQ>`eO zN*VY^SKdOq92^|_sQz@adhB}nawSaFm6aWZ>Ns0NLqqN9>FFt|zZYbCx-B&)Xl)N2 zS~l)|;Vu0HVAB)gpsX!%!!`~pV3-Y>t!LMHYrC8`n)xs8yH-U#HUb}?`nJY6dyU4a zHt9PPzLul$5$59kg!e1g-b)K0AZ5ygtO~ZgUdb7=GG&+`7$6n=PtpRgi0pPr?wFTE zmul_gP7%;va_w+DR-D-*dEp(>#4%WRrtl)NoISF=rtC^^49K!}uxrpcJJcgvaE5Ze z`mlsTNcOQEzf)cIh63nvCIi`J4anGY z+Fs5(%4ig`7ciE%&=DX|;GgyNF52?)W$UQbKcF{O z_6KBi!&YIffJ=*SoJ~(b!5yJ@6v0%I&xCNfD7mN-zlw3?+etFbEs^C{4z6yo*ym=oyYz29pvpX=3hf-hl5e2J`-JQ_A44sk}QfuUV-N)1a(QAQ>$K?0= z8>0WXC_gQ%obh19FLb-~KLbhYnXrU;fp(JM3Lx>;p8NaFq5-#+>`_=U!^()Z8ylPl z3ywb*GZdvfLjfaOe>wxi)f{D>h1gMUpnG>qK9Gh~s)J##2Z7EWKwV?f?Q)nG2vKXi z|CQ5cCNY{}uAU`|6W2I|bHum90ib?G5m5nLsqC|9$g7*@r=f5~BN4AFH2t(g!r(3F z`ZgM>YWRsy&*RZSXJj2x+hM)ZR1U;T1;c8z$L`P0>}yOl26SodZSRZxig_ZHRxO>2 zlatuq5`j^-(R%)Pz6`iAFC>^BG9o<%%Yq1xiU+5&Iy=kewfc?<{vKV?U2ou1bkYp zbjNwKrn!8R=c@S6Ehm%#Spgu*Gb9CQA;Q&0713vtnyOykuFsMjx z1@O7Z0we;$4#*092aW3_tvYjomWOJXKE9#yn2lLW3k&XI{(yaRwk(8-6(d$^rCc8O z(PXNAr?!K1pQ~yugMUAnEs`W`YHIr9|F{!*vEj895OB!rzwR>Z&}_fb@Ji`mZ+Rh? z4e-9&4cdDDlJUwiUTd^RIJ8_m$#`jI9Wb?AWZ za|H6ZHdgtXcWgB`K8N?^jM$5przhRoEUR_32lwMnA(;mu&1ZIyPU~&|L#?BeQ_v?# z*eI>l#iz~Fl^b2X4CY~^RQc~qY@HA9f$<+VF-@nKsCP!5JAze;^4Okbu=b#-5mD?G zf*d-S2a|LIlY0c`oS22ND5%l^K@^z{J*iQ}T4VOg(K z4X=AVE`SdYadinitYZKPv&UoG~RrsA$(GWcw5=gn97_um||TTSWf7bf&ip6bi4K?AKF9hJzX@XSw_ z>y46bgMO>q=3ow5e$xJ8vq)f9viS^@8s>IYOkBZ7#In^@av$*>HkUxw=F=|KfoHMSr9DxO`wSE28 z1e<3FzWFye?CCTm-QsL>x4z7gV$hcw;h9PxdGqFs*>}Ed{$rQ zCr}X=)3Bka9O7=;<-;H7=O9l#3891Y6Q|iT&3GZi!7U+^lU3UT{z-xTT<3O{Xfzuw zM3ez2c)c~P*atzcI@v4WVVB{7)OWq07`&EaIn_ylUq@qN_c;UQ2$YjJ6b6wRM8P43Lp zvVXjuUAoX_9q|9q^xff9zyJS^k&u;@EgWQX?7caJ5)RqqAhNS}$*7DY<2Xj@AlX^R zK2|Cu$KJ>8l$n|Nd-eW&e}8aYu5)ni^}HYJe%+70B2Ej!*2wy7x3lUUSX-awPIHcy znyN1hev0kKw8WK1JzQ0I;l^eyFBC=77VmZb?7-a&qTENyNSN zj-(TVHKH2~-^g>Q$n0MPyuKoWdAVr*Jd7%W*?67-PXMGj6?8Z!arg1eZwl!OH)SM1 zR3^wVX^XlFJDtqL!<~=Yf&pa6nrJX%gS%Y>YeN47w(zq{kIEfFRp%L zd#dEGhtF47IEGt=w5pXCmOEH+zRT&BoA$D7TM*!=Zf}*IDw6bg?^7V@BQd1 z@Efz;BB|<^C)DfT^Us2a_fpM;W#x~ffw2<={#0q{D1iy#e}_o;<`uU6KD3Zs%DMNP zN2QVX^DEXm7XTE`N%yJ8utEFBCMP>lCZ+lk4jt!f#LL{+Ont7<)kNaoRAvqiSNplS zxz=waE@PEN$r~FRI|9b;>mPnw{ibh8^gNT-F{|nw&%m?Eo3>rAb&Kuy-G)2fwO3Z! zkcmO4&i}q?s&bwF)4B4zJsx~I^tzo<(6H*@ya#r17@|^s2Qfj$g$2hBwL5qKhg#_L*a#CUF5nD=W+Lxq(2%ANc^M z106rov*kk#eHYcfb!J&hjYwey#w4C-qcA#Z>yzt1LEqZF*F8XA6<9Yr1{I*C7-=fE zD0)6kZ;~7y9@=EKu8in&U(^C`=>Mv|m^~nb-@&r!Hqx@mxc~6U2sC$ah%QD$R=Ot{ zBr%PRM-tT3)Gpyurt)eyVxGYA8*{M?a}R$~wm-SE`w%4RQ0u&>Jdh>YQuV&QC$6I= zahK(W*1#ulz~$MWI`yuku)b63gtNwLM}aq2&-a)A@v_&7WZ7}I2VtjOG{Dp+069sT z8ucn7?VKmRGBbmYf$gXL&37BK&5t(`3KO|^db1L;$}^cp6XsNL$pBWpU^wZ38Q?# zs391rRW)&r&?`~5!hd`zB_%~aDRgIC!|ZFlPmT2AVlO7hHBGusnMQfD=Kh}Xz|Z>J zeBgG!-i8`|$L;a^tK*cSsEa>Topx=$OO;|`Vw}?zWW0`(y3Iaomkt3C4i^PuAfNr|CSH6<5Wq#oW+ zPS#3A!))rk8;1!t?z}A!YSpBbK?#1}$&yXJ8`#peGSO?X7^#U1gcb^oA(2CUNihj4 zDid9yL=nuerDHN^Kl2PwT+Tq^rptX;P0UJq9|aK?(7SmGe(nGcerJy2K+Ftz zDT))zrnKDFeqr7q-JtH2P!sW~*j^#sTd^kOd-e|{swwRBHb@~(Q zjX=<^JMZod3!WVi1hlmBiCj{8$#YmMA31FX zX(gVbVfmO>Cm6p^7jYMNTlYv7f^vl?EDKXQqz2Ft35qTV6L7?>fa^#h{>mFvmDV4G zrGw0l4IW9>k;HOmDbR>$T?tb6iyY3es8z9~pJP(cA{;X-1Sv$2;g9u(jv6pUhJf)e z$vT96Oc&(B883_XgQgPuEOA6Re}&gACVFOkebk`?^ZxD}#G%L9OyTM2DM2H#7`l;v zmB|Pz!Z75x!A*@?9B`5FBVmd~5j&<;BcSd^zePV|klB)~V}&0*fi9AHXV*&>i79R; zms_51x9%*M92=Af4Np!?Y>$tPnP-1}G~wm#%{4bY-CT`Ukl5z@Mz6Q>q0&;HTXt}e zzhTAyXuM7ScqT$TOt$ zKj~yks;)Ke(fW{vf}j~+O+NY>#E9)^x^OO)%58YV?XJnExa)~_v4DM{Hd*KfxlGU&`MtZ&}N^Sv><(c ze!fF_Sy_?^yv+xmIyNz3-#H-O`bYra|CQH=dy^OvIsiQ&*g%OFp7``1dL4Kqy_5Zz zpI``4F1I~VZM=Ilw< zxGLpG-4tSiN6`Cf=T$;?xnfj#c1gXu?4GM*@0>MrL^nJ0Ha{`)qpUyv1;E4tB502v zZG+;aanwRdgHWEv!oN~tGq`D;H#yUuZd}+MTFg0hY?-O=TNvR|g8_?=9Wu`C>$|zz zC_Yz>{#JRCa~*OfZ-O3-Kk`ls6osHX;frAP+gB$)uFgi*z&LZQbCRk0UIj4>opa1< z{!B*Dv)#QdLzz(7gR9Hp(?20$8E=JbZBewR-C5|t9H|v2s61*q0{^Q>JHdNlZmum# zAfv2M38>9l07S5W*)^zSeC41};gr`C&Qy}BGB!3I+8Jn=A6y4@ZfpF9%%Mno+_mOw9Kxa2S_VzBYx_eHNBklSP2Xx->kIdkPpR)#~ zqvUTM`)=0udp{61_0({aLtU8boZCPFbVzNKB|1$SB3Qy5v>17eU@D!Ros}r5cU${p z7d+p9)|gsYh~fbYH;|xGxYav^CslSpQ`&?iZX3~N=ROR;R%KpoaVsKw;9ip z-M{%EXNwlE^%|r`Yis{*tJH6)g-ogr#^0VSvR#{jz8I>gcz(`hnrUzo*TLiA1|R_W zhlC?WM>p#I9+I833YK>dG~#IzX^6l?Z*^ywh>UEGHv# zJ~kNj;2pP!0De8qT)O%lTK)5(RKWURA3(2|!QaoOy^dtwqG0gm5L&D--Joyd=-OeE zP>2}W!=Z}Nm(f5+K1g#<>1>S^^liTH&%r}*Ip9T1Z2u-bL%e`~O!Be*l&TqtWT@!e zPdz+5t7Jw@{QP=sWcC^@`_bb;oK(1s{jPQ2T2C1zVj{U5f%)hR=Y$8z`Lg|`==mMv z3AO{@`PyUO=?JeTQM^r{3XBi#16N2)EM{PSILCEFu?KTee>9rVnW%hF5t-;q|54F( zyVd6B)m8i@?MAxr!&O6E2#>hv?&*&!H=~)i1`Z9Fgl&cHzm(OP^?q|S<;#DkAo!$q z?;LAqTCA5`I0i{A4YO8=XCN|a|261E1y!%EuD*hSecswiBM0BVIl3g?NXmQ8&%m3H z{Ykv+Pz?#$#k)70c^e4-y13y8C+|DF4}%?FNDm6T8hxFMclrEkn*P>(uU2HOb6+z5 zhJ%ullK4vxGaN+vPWjr9Sv1|p*%8*cil@2N_E#ghqK{#}n9QtZD#uYIGRuv(zK@%*DZlu>Yp&iIp#3(*L&OGSEpXBp+SUoE2 z(e8hHkNEuOt?cJB+%U(pZG?B)LD0{OM#Hd%F*p8xd&KwjF@8cO-U@|g>Tw+SHV4yjy|!9xK86q3+UBu=gG8D1O8{B>W6%_l(Nu^$SQoK;k?uI+}!~M%r|# zdQ9yy4@_?$?&e<1Q&*gN>pyd|GA~c z_SnA!y%D!_c4U@~;m#}V9|3=7l?2!C@C7pMPMPBR{L3W$&wRjyY;0i?g)yW~pU`6I zO{&5}_%Zj_!K~6&{A=fOhq7ODVD;xvrnr652$hi3*)K_n25evtZYYaE80(4i-9j$Y z=HB&z>asFxM15U>EfxP(rb(%wMN2gD4deEpSLO4IibS7o+FTUsW!TiAz=M*z)*o{| zer0x;miP8wzEIxAtc>T#Srh>^;ZZIxRAiYlaWzLVV7(pVsi!$8>d`TYIH7X{c5?ti z-QVe))y@!T!VB}Q1M*<%ps4qYdVWMlVtI$vCT|Pq`|suJcwXoRF-#zDa5}e&TrUQ7 z{bsq*408@8<4ui;BAuu8_@zGzcw$Es@6h8|&S#U^v2JV0rRTD-^YNeOu48VnSTYyW zWTPHGcnb=#!O0I*+?i72Ix*b6(!wiC_2g?zZq(BFdOB)TVs0s9Y%^p%#R@@eIIIin zE&}tKM&P=iIc~Q3H@A225u)%f0lEBDJCP^i%6b@d$+wa8l-^g*(IrO{E70ZXkk|h` zb!Zcjw$*@5_1kGqBVVAijm=9XDTU^O^4>@~UA>@3aTD8TGx$op7m>Hg>B6pe{dUAB zAWY=zGUHOvvav>S9aQr$n!dHlGsfLYjkLdEWpR{!X$*;i56%PZxD{kV?1W%}%b$46 zTr&Fyxe)c_KXf{dr$B(=go7VNTv@>ovwllup4SGN4*0z$2kjAojN1woRGTDsN$&aU z&$8KOE=qNk2su*x|13ZhH~u?}?hcOtyqQo~-ODoJH^+vEPOn*Y+9CejNQ zY746Fg8rrC;Z5#WsPNP^4x7|c=fMiq?beCuY3q+~jo%df;w%Mi9M`1sziBfmHY-D` zV=-sribF#~*}e4yi<@(1ERoeK{i%y7Im0qvR%QeZlqAixT{1|xAr6B5-u3Zy3}4v~PkuUhRZo%@lr?E!slE`MAwtJ3 zPxE}3pvGmHL;!iTlx1$Z`3{5wU#AG5S(+9l(fcOhbY5v zX^vOg1Gj>2_;#X)&F|d_7le9(q6O7$YZ>`se)Chy)s?Ra=Pr5~@Og;7P2Q-7W^LY$*Fw3BYLE(;- z#r+c>^f38a+n3?b#%ZsFaR<=3RinU9_+U&@T;;R1^wn^RM$l+%02Sk}oFs<5Z;8s*6(iQkJ6F`8YdkDNBOB0KdsU)V^HoHVHG=PvlIlLm5lJ+1kJC zTV6(6=LwyNZfk~d+9K8?34a2W^{a#?1QD++p6B@+fXIE&prjV&Us3U&=ofIUzPN&s~ zaPzuRm`TpGE&c}7ZMh=>g^%i~#00GD!!3UZKw58L?&2meT5RuqH$E}X`D`a?aeeOU zpaqeW6~!yBv=vSE=6kM#hI6JjZ_QSx1yWZj^iN;{HY04=UL9*2cixDg4PFqQm|Vsy zoYM&!dNWQ*vxfjr({fzXu}XQz@UZLA}nG=#?Tg2`Fg8T}`2!%X^Zhv;H__=Dzg$Ob#9bLQIeSDhTMHK_!%M}9cyQR)a z7WV+iE~KT(Lf>%a%iw}oK!wk!-FYjMLeF5!+S+w3gDZoMoP8!JWUzt*OJPJM`rk`u8)>QuWI9JI8X9o`oD66#u$E~PX;{p^w;(}q6!P`@sxNPCw z8)B0w@t7aen4p)0?ReOSd2&pEtD;;W(G`WgE09#U=O}%9se>Dr38_Y0*!c2ZU4Umq zd7SzlKx|%6fyDK`_tf?cB33DS2JL(DjZ_Od-aXnjiUA}|G(9CyhP0KHlr9B4_6v|q z!&9{`^3Y>#53$SmOU`#ZJtkXL$uW(UQ6^M0bnb*V!iL zt8g{u^Q7N`DUm5jD{?w?#1Ako+3ZCjsu8^0$3pvMna%;Vv%y?f&YfFwK9R9#Jw6psLU+7Q2 zH~KN!RR7-A?7}!CmVKtyo*F*9#8I$xQ3pR_$G3ec5sOiMztux}`}2scZWFOS%fHXF zzCwC+#)pcUf!(%@1a^z~{nO;Ian5HVfg=pVEW?krB&{>{xvC6y4?<28r&LPdSqkG{ z#>yTze0?NsGd4En^7G}J6y_Ng!!uOCpM{N-@-NkPiN7mjs*|X% z0LRj2hf16H6jxNmc06rQbns6HD%{uoQ>`9MM!OZCXqkF$hTPtBTkcKgs9y3H$;`{8 z{u3Vw`4mR}ZD2UdsN<5drW1M5^eVDb`%>!Rb&d?GHDO&mnKT#6a-PerZFm!-L#n6* zS&EUuI%D3Xh3ps3CW)MBZV>`@u)u`v@>ZgMbaKT)>%Cdh2tn~hBgaWHT{(w`cU4~b zLgsru8zp=64T3u0z!hR~i#@~1UTTwrh{49I+-)vLKe@sbs>$zCJOwize*@N&x9A|FbPOoe&%#z92AA~`Jax?u=Mn97B*9I9PHv+h&-U*g-l6MzJ2 z)LpIl?3!`XVyn!`=hw>g8+STqJVjzp0Lix~2+Ebd)N7M;eQ0(f}!L{wnX4AIfCbErNc#)p`kZQe*tVkf0vU#-X1 zv9oevRVSH9-Bg>}J$rO`$og}wCouB3{T?Tb?Hi{F5;!#Z#r<$g68;N4TrGmqQa4k^ z0H=ugDk-$00#F2U2z)Ta#LV`?iju3|BgW$Q?_I1S%9my2e@%APNIAF{KiUZ>kq?S^shaBUepltOUke5AFHd?i`iU|(}^dR zeIA~kt8eB0k`Qz=Q~l zkXg1Vz#8SuGAuSB{f>}978o=4ujg(3{CSIYvLu;N?86vU?~8uHE*20V1Qd*OKmo0S z(C&BJDf@VZM8mwT6?}bCr0?}AOT5BE9D#z8ONaZYFqLXpRO3M5iCCF9)XGakre22P zOfSo!0cQcBa)wtMguRZCC@1mLm3_BKZyhx|DBL^}^5%~&PUxy8hcdTaL#EIXyu_E9 zJ*{?nVf-s8n~%-JCW%EGESJzX;`VK>lLPg>pu@*1J-G^;co`+1CDWa39&BePSyv8L zrJYMErz=>IaTI~}lah(5l&kqH`4_BL?JF~RG~HsUiq_@+7dLv|AIs-56G085D=&Rh zvHUQ3e^|XU>my$s(41u7gpt`dQhAO8i|RPVXoVJpD2mHw5Oc zilqCWZOdB?TmH~bar?qT6l|cd9>A0Wt4Y&An)?qrQ*_7cU8`OSq_F-?G;8jZ_#xPa zV}y0rzpv7L3JPA*S8ezk?YixdGBmQd(@h!GX~SYApG-RL4tqMdc^eu|WQ_6DNErrQ zWsTh{`m)2SkuyySoh2a$DHGc)bLk~4bG^ArXnkQ~Gav6W{J3wtmnx|bKQ!oF&aA!rg0b9!c!l{hO?%9oPko-3@#|q^9V(`S%6`enB@*5=F83(nz zTRm+$*+kdBXGoe5%}IZ1lh|z{b?HkFY{Wd<9(T>4qqY+&4KaB0v!m~(n|^;pdG{WI z@_n(8)qI03e`RX9eK=1I#N^(uq22z1Fmv#?CotD*pN4GovVu!}qC3k~;BtaOME)dy z|I#**ais^oL&_efJ}r3c1eYL@IBw$PGHv+sNP376*zy|3~$ z&SRed5z}E5c61SfIPxIB&vsLXkAP_Wr9*@hpW0AWw97F2a zm>2`lJIG4U%len!?JbqyvZO_ZI-{ASaeqvKelE0#Bgs1Uy~5l(dF&EM8!4W>P~Y4M zw2%zmco(-3yR#C-(tNDWkt0AL#-F4Aim3i8kZFO1NKWR_^+diD1?P>{osQPIKt_w( znTAhp1^g17G>XFBWc(nKjBFoa2?AnJ$LwruGeZ?)&%JFw_{)aU-Fo00z6BsHb>*}+ z4RtelO#md<3E%c`fx3Y61wE8@)pi-hWtNQ;q$IahI_MNCI{Vb2XT;;*W9ebz(htYp zRhQLETghY_nCfJ?S9LoxM2|?>h;USw^4>C!n!7HXuSU*c?6l{jCN6ra=Ezp{p;wmi z`%5eF@sBZcFe`3YwMh2m3f|Hn0N5b;;u@lb~@W(B@SPsTRI0`v9BjeFbF-~I7CkTY23P~~%UX86DoDYW?J zj$S*e>YWI=t*L!AY)6dok@cfy!Ge4DQ#+k+7Xsf6`(io{Fu*@8!#1oY+-O)y{vt#4 z8#OHine7{-k*nK-Pq@QNL7l(IGgOO{E~9+jDRnMJ?J=@m&zZhq+qdI>zsMyfA?~~u z=3rML6)<5BBPC|}IyFTdDhg9kPr)9rG$P?Epu zrBH5S^5Cxgq!hta7QtF+ZY#aX`{K~~^gBH@QEb=WC-#&6s56P;sGLJA> zFS7qIiD}_&k0jgz2rv-rc2|F-vwg$Z>--c)hzLSvxy!mQ)kYkw~K)Gk=^*Q#C z?giaI4rqnnPUy}^!_Z*khq3RE9pSXgX0%LT)X)OZ>76zL8DICMO-n}|h`*D|8B1FN zD$k>`Egp7f99ac=$k!Rx5KDAt=%aD3}on{oT~i&_4O& z$8LE`ON)asQX1@|6$LD4Pg-uZb9bnVpT)fX>K~)r=|W%(Cabb)w*C_c2|%jwwr>@B z!1s2$ORRR8x5YR4*k?nVmBL5U58l-|NB`2SO7r~6LPVQYZ{h2d`p5QBs65m&*IK4G z=`t&Ibh&tKpc%(KK0Y8`mSD-Wj(Zw@(V-fNR z&mHj_jW+5G0(TXD0YSxA)X`hi{B}-ohu0pBa=T%Xkr$o_W)M4+E&e4Q`U+WT1B9pu zcLny{Yk|11?BdVO%{GW;uqSF7TGTNh1*Qk~tmxK`j;L$f%~eA1#%sGh6B85MK!mQz z;BmUS3*7Stw_00cZ?OMs;4?X-sXM`>)X4vx&Nz^Q%PkI8LF)D}$o^qaHc0?r5Qtzw zB+&DWdIs@c#k-~|MkR>CesQvhT7%he-hpSneYdYEjoeFL!t59{b7%(pHCsvL^z#ky z#SWWGu9u2Tm9pt@-$1NVQ`VVAFqm>PTwG9Y7;l#G!h>XYty^G=YSb7bvwGpbG1|C( z@Rqp8@nvJNpaB~)oEveDe%{Xjz>iNTq{aI$Ru)LW)Fr3b_oA8tV*1;xA%&SDm!Kab zBYUMlGA9b!fEyb-h(p7cXTI)9Inp|2Ke*m!-vDtllb8w;O! zKwmo(q4?>%2H9@z;Rc@|p5w5NG)#j@%^c~rgwy9+cU5>6-kiG7d2}c*^7Geh`daDW z<}~jU?vn7EE6&}IHTf&`gH78ycu)Uk2QW0s-E*)DP*DXdWUJn^zTkv<9wexiLp0qd zxdIEXD=?lkq1}aJV?T>wT`(LkRD*vL)M@xaxL3ntt95W4nzc3eBOfFCM^guLWq+&M zo_N$dT6aEx!Gdp&mmjgS6n!!=GBWC%uLdey1pS9BD!ck0us75b_TgJ3hO!^Z%AiKV z@@B=NpQgRu4F|0cB3}+|hPS(a18(KLzWm>!V^yd~*uRG|2#x11Al5@!%cRyvnCqMX0z5|lLe<5^uCt&wi)=Cf`Bdh@PvNxMM${+wO2;&JJW2CtO&+5qzgY!{H3y5AVYiNTo#F z4!BeG=TWL;U8yhEO{Zjw#g-0@*&V|cGqdHnmx!ROgYs3&qa2$U_w5WDeoosDez&0d z#FM&~>Y97bcND95qV*b?bHwk+?bV}wi2a(K!ZNOOvPS*vjS$zMxp%K-w(blu@;ppG zl7Y$_8rovO*Xi5&uQ%^8b`53`7m|M$xa#s!kTk}HmSYm;KYq6iT=QA%P^Sh|uV>bE z@OB``)0lC<@D?U=kvkm$l?Qg$ZPv-4nR<^-xjOE9$ly6%2>asPT*L^#mlgpDM?IN$ zXK1j)LG#K2obmO%5NV-AFRzXo;+g~E^W#7s{^AvS<<7P$Qi$^3>$ibdo{3yq(&WQ~ zK)cV(UIUZK`a(`#-t^6ZwqYX5)e(LagM0!6fzK-`|M;^2yaRw8i)jZ?s2e$L0AMbw zUgtYE;3+e~c_=LHxCK`0umz8VUWe_vycOt6P4;g>SvPdczq5}yL`?>`LjcERKq0x?w;S5^eACtGt!_P3y@Aok9U^FG5~PjT zLcs!n?YZ2RBNeOpa!j})39`Fr3 zi|sU0lG@d=OF|!lBPli%QLV_R8-n-3uhDf?Tz(B4HVx+8EkPF=D)F4$4XEJr2!~R4 z0r7K|n5*~UA25P(iUK4aj(+YLF;GlWV$p?j-i8xT_UEPG3y;M9(Lxzn*?eWd=wFJZ zvv_?*lTnWy+3Cs1YH6037Dh>l7yhzIx+Y@-hi3y_T%G^* zQLg{*otEEKoz$F4fi}=ogoMyS=GRAv^KGLS^lo+2T#E^A(#V~hX$2mS)e&BNKVZ+j z2bKs)b0$FeqOQ~|LD6S<4;VmHjyjZiqE#Frc$9i=1h!p&?K z%gjH@mT@WqpD9Wy@aGS)X9sMqFM|+jX$P}?FIiFtA4!wDN+Ulm*p$l)7yVQI?(lP7 z%hO+q1920V=2(}QzW$}nUmQb$1!J?wav%9QSyh8J7zyj#*c@TX#}>LcONFx@2IDhw zmY&B4oy@y5bWwB{fb@HS%cz$~>tbs9@TLOE#6-u>Nj=B)@Gu1lu6ziq7$%7!U=w+b z{pHE}p6}~oNg=KBOCb#krd<@exr6;l#659RJ^d@Hcz(P@(Nn6Sibo-p=-`+dj6&Do zyEuDM#HHcVy4^#7ohre;}l%Tu3$UIkLA*{&S-ttcIgHM=OM8;kSE`7gHEXlR5 z-FX<&sT$K7Q?Rbe-biT<;+7<(g&Qjj*GZZZQ%9!9s8S#bNY$7`OTuo_OcWX@(QI5$ z`v&p!>djuag6vKJGAj1}G^&`RkDAlvZ9rY6%j_0`DA48!6V@VefkmzA-A6``=Yt)Lh8OJAfY>+@UIY^t z?Fl%uWVpeMXwg}_(Wt^xVclNZQ~5}j-b8F~5WPiEnIsa&d$PIi|{K?HN*25L#%;A6jx z$DHs9K;p^*AZ#}HLKZ-ya*?M8O(L8U6#q%hk$P#Sn`U!2tW)IhPv*2k#p{bXPx6qe z*vBu>+A$8GKL7H0l01)p*~r4}&^auH=n4>5jrH!lP0-?rsQV+myZ2Zv2igJ>R9Fk? z8ceoFgP$ZY+!d}onPIi9e|hNe6eRgG7;yTWnv+f(NZE!&A6^H0w`pQ!8KGJZ&zk|K zEPs{d=1(J#FaP#&ohNInYRrC0o(O>#pU@R1LBzcM-hmYBxG3hQsG~2)Sxw|g(`$$z zVTJx){_Z)law)#8#As~sV3@c}c`AU!>-aA!bTR;4dHsbNt? zqEh$Sz9dD<#RS zIXlIs&D(i9AgvTDdO1#1s$=OeNk!{AB8fLOIK5l@8Gb}rqb}Y*PQqV416QD}rHa`S zuCLS2nH~j{ucjFOuhkBuCL}@Puyd-*>v>ce1wMK%Z>aNnb!{$nJrpd8x zzI_IU0#83%Okb*rKUZVv#_edmC{=(TF>jxyf2L@MmWy}9h#Y)+#&Y#d&_K#h$T#%S zs2=tL@$Dn?!a#j|H3ZQ}g9E>!s_J5j7bkaGdk^O1^LT~kv~ZIBi!FH{*Fi%`I>7<1 zPA_6)`fb3L<=fW4zr~8KS1`i~5=(TWk0!pXVL2rTR?x!i`e581z;@ku7<6vE;my^L z$m|aXB;^8oU4XMo#56p!kXX@@-4Xs_iItfn;lfJ6Csy9kUnHS9W4Tv!O0`sI^L*e= z!&3bDMCW&KL6p96$V}AFMpKGz1Nqd7sQ$r^Y(aDp4)2OQs0O|6cVA4BAN7PzyzWkd ziA0yp|bT5n`JFe z`9984CM*XYj4$x@)`XsAIMgqZKt!xZ9#(xZm|}QhZ`vaRu8iF;n1|FfGY7u{3?3Z$ z`^A2IY}BY@?W!AWRas4$V?vE!NP>aQO@SP zhR)kp%0yMamgK+vy@X|?k>i~qHX;W3y8*Ta4|CSA#@Obpx4v2ENM8oZ_k;p<|J&ZU zKUxr*Tk}mC4&+O?C><)w`sJ#v#lFzejgMzR`$TW*Z*R|#N&@G7B%~IYyF#b~B}9)X zcc~z)CB2tDzI~1zT1!99Xl(b2QA3+eeW(94?AW^i!<3lc+34l~kOhaBUZdg4Kb$sN zuG2kMaP3GG>`3O6E3vHd1~>~9yK0w~qamY6ej**;Ug<`n4yuxcec)}mV*I1qV-uTk z`WrN`0*$jJTdu)ey+A&BP|*%Vl^fFPxyALLm@}S=i07$2^evO{`bH5G;9DkGRUFur z-4vre$fPDbv4O-sT7F-=PNbhZUDwA(+D+Kui)0liH^&qP{LDQmw*uNHC?4z;qV<^7 zgrAl>8x}WH zNpKTy?uCS**a?(;bI9&^b<3O2Jvulrxo`bv>`24YO?XfT@hGKpO{DpzUTfQBY<%vHAfV-5h!Ap}B7y%jzFrMtRskD{ibs zDR^o}=9S8@`~u&`=yT(=xdkE5$?Bu+$ZT@{n^c@wPO8o+_M)&SzKaxf{}eCUk}xOQ7s}w}0?zQt#b301 z01(-sXKYoZOJx2%%^YHG7P)9c78oDP?FG+9PTRmI;BbKUNGPDLnCjH5 zbhU3w(kr&+WGZD2n_sz2Y)4|2)Z0kuL9 zg0dz<{S)H6fK<79zDa2*NAR2k&n1Ux?bzj`IOd+W>EpV|DI ziOFMXVr`7sa(wR|$@b;noc`+HRtO<4|iM4)~26xp0Re3)^Dj&WFtum0BnS#G}J z|QD>Sw(yqtdbZL|I&(N zP)r~iF`QEz{=$r4nfDxyrq={!$UhSSDf&$=R>IzaSm(R)DO!c<#m>VFF`$F9DFjLZg20L4vVu(U zJ0j3qu@BBLr~V~ldXR8)oDJspkqJWY8dGn;^ZjW%?opT#oz_w09K=HP@39#9i_1MyB`zwdEWVZQ5OCHV6>#gA5jR$mNZ{8qm6 zbbgpx$HIWrLPxGiX&t=r9?sd;Nu5M5XZpiiM+Z?rfm`@@2$62lf4t|vJ?T?=U>X_@ z}nN!dnMah`X3!yI6hSW>>5QC0ClDJv4#Hbg7N4N%Y4mm!mwyv*PWy^(yRB z;CSc(IYK-m(BPhvB0=pAl!e40+3WNrQq{qZN|kF^qi1j%Yf%#^@`+%D*Mt@ei&~;K zZq|QYlVB8QRJgm%g=Pwo)^u>XEtY#FId{xQacU@Sf2Z3bHQQjOeTwQ9^W#=1y6~&Q zj(W@vk%WPgHf{#M`NKp!z*7^-d)MEf(=l%K`LT8}m5AhMjk9DXk&YkQ4CJE!!82ES z{{9YH`O)(a=vt}P=g1pv4ChyzP#Y+~%gC|sF{lmE-AJJ3=LW$@pl61sg5S$MN{NlR zKr}LS!^r7TCYpWVIO@IHbS~3jg&W5gQARimL`r)7OSE@$8UO1v8Ls2@RLY4;Y--q| zEfB(|yWZ`6jGgCxKFt(dT@%cWlvdY7HECZ}qt5hkbS-~&Ix=c@C!)2ld>E%2z0-sG zLIgXY(C$oRG>MAkGSm0PoNSUGg9St>??l={*=@ypnx6gcrctdQ1vJ+%!|J=DWCdFe zNLkN`@b-}_4eWok*0x)5QxETdk%#Ja5&FXVX8)X>_?#ky>x81p{WA)y2|wEZl{*LF z-xBhm6fc~PBFj~wmC2n}gCC_9ymE!Begm!UnI{eTQT*}g32hb6IEY1GLBs5Dtyp5) zoHKi}BXyu~NYnr)IVwtWripE3@$RluuVcvh^Ks;Ge&D1(O&@i5cYM1x;)R$5)dja$ z$$jtWfo0U2NAo8FWP~FXcX%{;5K%|`fZq*C#$dY;P23G=VTG>>AYzu~{O6w98@RG0 zM!Nw(eDU5^2l>ta#%}G8;cgQX%dcEwaZ>MWnTntGc5|fXf+x`qbA$fMyPl3LkTbWZ zLzNs*8^adL5Vn(uSb@6OB!yO=od+u_1s%E=TM-no4GFv1$-3tq-#nzr`z z@@dXA0psZs?mp^>Z^1sRkPk=`r0;t1;*$2IHx)rRjNbb}a?O_}{!e0}r_=%kxhDc( z3ez9kWUua?%seB`nf}Cc|1t=Z*H24^dcIG0o{H8EcdrAq%r||?J8cj2vlILqLL6|K zfI9t0_*fbL-@bAud(uz-%QL@%@TLp(@L(Zs-zL4!J|R^T!ZI1=WS3t z=n=iOF_9|=h})B<+66JQrvJghhz`hA`gXTEr-a-3hF4&?fHkLGYgh(G*xCrV1Q zRU|Z72f%$@Y5qxx>GXbLyDDE>=cfxZ2cV5ADcLgGh}Ko>1_cxvby(+hEWi=I!evs@@><}}Ut@k0Cf9`y9;PfAGe7W|#RSJ#FS zWYUs3LhU1az+uDZh2^;61f#H46t~S>D9;@Uf+e({Xa@u@&3-|ozFnd79BCche0lmC z<0|}eO7{IiRF?PHgoPk4=)7GKH>ZKs@m4_N5*YpFSshNhqg|e$tTL6 z*YQ34@txH?cj`}lb@BuWrZzb%mx&lwOw20T>IfEc!Fe{J)9Z`(iIE`bQD*&g?L-3a ziLiX?KVR_<8kY44b;-i#tdEy;G<6I{Y&|*Tzk_apCzJz(?;RgiGMLc z?5i9-p)8wHP5s(Q0Qbi1E3~2qU;S+CgP3$|EtdP`g(##>4Ep=PE-T9Slu&9~QC6m? z$V)?f1ZpJ9+rSd?2{Yyz^q?ernT#|Qj2`9oecMvDY*n0%Cw zPVY|4gIE;UF0J59Nb=dLZUFo4F`}b6UJ*cGd{6;Vu(dHP3;j@MjD(xyGHJEX%aY2M z&$k%8seBHH!zbeFRld+dEpM?rq_!0zOh)EiN#XGayV3lS&Qva?X=D3jA$m9v@^>Lf zsWB$f#w0$kU$?YN&_RUFWy%=Xa-FoPCx2%M1} z_V+>)cL({KJblb79U5Um#6-udw#rdQ=23rO zKzPuS@HgklWobxV)H3PYX&z?!R{h>B{vj0_(vF3phvJ+R*hne|1;a2Rh2+<>_G&lg zSm|4Xq=qMB;|Uwtx;lmS=SMm%@vdCHRH%tvf+UrKIZg&z{YF?E0;zkPG4Zg4MxoG{ z-DiwwQm@za5rV zuj6xmblL{w^7wewa-yh>rrN^yRO$Cni6{5^Bgyu*>F6DZFO^hfxkKJw_d_AADMF}f zrg^f0e9pAj4`BR}PeqRNug|4(rI@~~X13=xF{5%@JQ#Dv&&g=!l2nF!1<$f z)$iPJTk4aDv3|lJ3BQfcHQn1*K!eSa6QS5AUYF~$E?8>Dv7WG&eVIlt_Al^CWi z7aio`#IgX!o`Ii~S>glnHiUN038JUS<}6}2gIsME-ep64?jT{DVEdY|M~tw$#aW#Z z)<%YLoMS|UJ)7>ehv_kG`z9v3K`ljttTfH`pN7jlG5>T;!479PACZDYW+Gukxzygn zOc4v|y{*nvB1cLp4!eH^bjwHM7CT*B{?H=gW`av(T|Rm9dhinY2I+8gj(-Th9oc`$ zjF?f+2VWI{N}#!$tA$9U^;OydDU_jh8flb3M61&q9;SF$A>$E5v+Ez`t-#I@w8ihL znlfkhN8+{LHD8XezmX|V28_u9DoHx1I6f7xIw(!56LAYCmhn~TAuL`(Vte>izpUG3 zgx|$QVs-HBUJFRf4;8$jT-B}3-9@BTlb}ZxF8$mSNtyLXpi|Z1nmh z#~{$T70!WqY2bHGxF{4hnmYOy#kC(>O)PKlSYjezXmGH?(__E(ibyTLxgK=l36&2D zD3~{}{3Ql(Y*iNQ5`R)T@YO)hFZNqE?>%&3bB_A|wfEj%O*UP_XhJBW0!9Q;nuGw- zQF>PiLZ}MTK@gPQdnf2EL_tcB-Yf_zRp~t_iZqelA#~|T4gE~;exC0=?^@>%IBR|L z1FT%vHEs9VvuBSjmr?#sOqN$|(&7~YL_*?5D!-ju!&X$`qo-N%$pH?SYx5ITU-rw^ zYK(D>3$_+KxYcDHf*sn$ab`?DlzKjB3-3>$0UI`hS+A3m^&mWucFIbu@5||L6FI75 z;)Wxvi6bf3B~rGRIv(okE8 zAIgo|m@}&Oqk(iu>YsI1pcc=oh#axbIqlvw_o)te*7VT1qEk(=Jz3IMenS%3QO>%kSxJQ4I9nrbgWJPa#RlA_35te-DTkHN3 z=JGK-gV}O7yWTxbUjaQiTEuLgMxS3xOjh5A&?z@ksk6rem~FX|{mMIyYOL^7UlAZ2 zD>wRZzNt$s_sN@PW`%#{s--;$@OP#otX5YPia-aJZw=tqaYxEsODkjD@mQ(YtRsRT zen2a%jf^&4Oz`mA7t)-rq8%_Eq5J~_K^Jj)hxL)z*4FbRoH4l4+$cVsSnw;AZYinK zlg@RePiE}hP9cY%65$j&8ik@qTe=!p2sZbPJx$)f4dL(lS|7ie8FXY>#f!*}aomv4 zj^Ln!0JT+p?^-fw@K09mz6*Jnwb0>(iY!SpUqJbkqJ?!4hV)JD%?5O%jA)O~~!W0c-`EZ(6%7oe~aTb0O*F(H9F+Yg%1XcsPEk?VVC6 zRkg2^C3u!jLdY#t;~3nHf6a!Fd8T?!Z3q&`Auq>hNRgI&G& zNAN(h2E&NaS}UunkQEh0?1|-we(WxjdfaBLgUZITgk@k+?m6h-ZIm&44^ zOz+O#a!zPPvzh{W2dbz}7tKBBt@|E4aX_729W>}G#WH0u?gd|}V6pvi)(B=wwIn!S zB?h$zGt*PmNzLkr$=h}jtmuIRDkADG!Nq5u{RA9!T?%mS4P}%NLO}~hUVYkDLpVse zOUZ0xo8lEK6V#M{o&sm==ku<=1GW;0HM_nepL$fOjq0&3F#GvksJ{O;>cO>@sK$w9 zWG?wW60yK(N#eG+Xh)q-6iLv?IM1h9dO<f%CCfs<9+Op0xaxCmmDErexeC6`QvfEsA$*5Ds&dr_<*9^8@av^T6^8& zr;Sxy?C4-cFB9P|0g1NR);@3C@?sN%i9aa6u@?rJejW}Vg2-tVyH$JJx->q(o|FDn2w+h|KxFTr-u4K*I(+A zR6Sv+9McVQLcPYf!`(E8M&1@{;aPX`De6>peCGUdmji)ur;C_mBhbodAIU>%w7bw+ zM2PK6vM6m5-4c(T zS;N*JybI>f{0H-_kc=~d=3U>?+b-cVaQEk@`bFRs=aYuM#n@nSxj<%64^nf$Sg^zI zpvOi8HgyTOFa0+FAfLq3W2_mFPQU&ziNt0JjRhd(yS@z+W6ZO|4bXexZCg&5R7|&M zxxy7Y_q#Cd49SFt3=>WBfm?q9aVy_Qx^sJjq&xkU!C62jd{WyxmC#~j?cKfiB%wwN zG#a0VvFK{l=9&V*(CZJKG~x(SlbIE<8f>Q&{bf-ci7x-*be*5;#Z}815cdL0grkB_e<*TXaZ+Uz5wP-?)x`d@c3Bu8M?coacjDLE|AOLcqmlV+M5eW9yjJ>IJE*fpW^qY5F#SbE7u8T2RCSu`#vgu#kcG^-bAp)+ z0G8E%jhIZu+jM^wM(@oDeSblIA5sQmBj@%heN++sXIfRCGnkcWxH(1+T^!55H>BD~ zLY)I*vKHSF8k?+se8%*O-J|)p00jJWB=FutMP5NKEvtS`_pZtMW)^uUhfbHroZ%Y1 zH*Kgze#9zO6u|)~J8{$ZTBZX4XBij#lO)qAv-*SmH}B=m1dFhPo`PdjwysyXuA&T$CS#NJ&BDMVOJREAOyuQbETBsCep5SR_LqQ#-#sPPNaVD1c9>~JVuRya7|P7L4%tHgHxF9#q~;le7egEe^ecprs6^h-^0XKPQ!wo6-W<=qWv>w;2qG?;Ly|*F((Zml~xs<({wK z6LFzm<0Z7>*R;a&NH$VRmR@ZTu+Ec>c$EQJvpybAtSmVNEUL6Fl}FYRB2rMpbeZ8V zHrDYuvB*pP6VdpFTFoP6R1l!1``mWxv6@Gv5K)X9uE2G@15AtWS=J1`8NYKq(VJrB zIW{^0OiQ%*XbQ~fX#}p|w8Ssp56ROau_A?k+(88P8QkrR zt8hBc|5%qK>&c>pVJdN+r-a1VDije~d?&Wxw+Yhnl^$+|nDi&OS=jR^&Bpb7Nk-aP zqn`i_it=-!b3;8a4wx_bI-HnsaP!THz|D(JgNQLB5ta{8rs`b;D_{-mn``N)xSFFy zk)??;w@KJ{*PXN{kiDuT=+L-2p++vim0HfH0U!yJfHZ@6!y8n-_lV&BibM1vjVo(W zdDbE_U)U>>MAonuS(#8>OuuNN!r22}#BCA@iWZN#0pmsO1j9V&1G6({JAfduhJZNI z&S7o@sa~%dP^qV`n9%Lo5@NAZZ%%-^l?EiY9yh`rel7lC?&=m^E1AR3f?EU_uegMS zzhhP7u)73KEZ4TZ>%@=3|8$Z_4EPbW05(87@Fc1cvdHC!KLiMO(A~&dot6QBiZ;IZ zEE=2;huzD*|3e4|)xhqhF+9qAN#Ybh3NRd?2uR_tL|9z_9cJHazOYwO>(itheTVmOdO zgl$^1Xfui`0KC$I>NFPcyX^38^!bo}vag~_#+XaUXZ8HZe7?T~@g$u@5UMRj7xqrA zmZU72J+GwntWQ0>VMfrAFRYk#P|Z)4x^=7`DrS1*%yh3 z`u$yuAH$A91$~+VFB{r{K!S23w8+_D&d}{85~4*;5Zy&M0lYdc0lG?6mslxwnWX&O z`E>=zXrTp5;cEa)=Kv@jF(=5Dbfpsc5XV7B)`XVf4GJG5Ri2SUb5=Pd64XS|Yjor>lQ|F6lS?Ctz zYZ#5yK+%JqKIc8b_PLES?v*3%=!noc6Jz81q~z+djzhd{l%KFks_R0lJwjn}FjGN0 z>-jf8_lXwQ#fWht?!%lZZ)fE^$$CAUg{`>JC7V1fa~>m(k48)qN`{L7NoM{tEqBkx zx+BA^denP(nR~t{P(hJe>fAu=bja8+PRz>oId%vnOcQzdGybk zgCk{-zx5?n>hb^u6ZKLIYijEZlZ@w8rLeQ4bRxym<_7vOb%bSX)z07_a^G*KTihfax)lBFR! zS+`MfjgB4PO-bfT10q%nlARH_-VA4!aFpUfXkoEWkjol8=z3b#=6Z1cLq}J)J5i+& zHmhzN(>mkaG2^Z{2r{OPFSY(7A)<^r(Qin8)#5&~amqSwJH3BrJf)tuZn&elumheRzpJky6RP^|e8&9c;e*y0- z)p#oy-f@X zZaBWhGQk3=iBjpuKH~pxMg+Jj#oxo3aa5C4UpS}?V$U9fu<*`5Q=qc5?xS{r7HF9` z`QN4KnjU|IJcjN_JpsovI)1%YYE3?P@{k`dgg2jYmliZEBJlA*TX_(-$ufC%S3edz zP5&?cT94~tgx8Z_1{)wxBE-C4Yj3e2iYg4z4g{cH!2-~D;=2aAx=J8FGw;HYmkLCd z>>|`a*s#b#HF@fiC$JX~LR9xDg={va$Bl6X_29J3&t^pu=QgN!`J=}ZJx`%)>&;`9 zmgaNOctyqsY`_vmQlj*8>*e--zcr89l=e?PfW6PnwfYgmF?0XW;3|d{V1ujDbVZE^ zs?K?`ut!g>8sml{pZ;$JB;ks#Qtt}K$OBubd{1g-j&rg?feIkv$*o>)$Xr|Uh5=52 z6<(`#1QHvb{U^UZo!Wv5o!HmY6Zj2{KcXk?Rs4S{rxp&o*sR>pPP{o(d)xgZ%i1BZ z01W{{8f|@SAt#JfdW@Uv>Uu~wS3A_|8yKi4DcQ`7QifrfVmsz?lpFmvuQ%J7MF93_ z!kCb_@%kAljg$QZF)5O0%eBQxsP4VaLgPOnUHEo91J(=)%-^EGYV@OaFBfU>qHM$f z^#10f3{xQ$0RPxG3ySb-LI@#{A-?lr8o9vpfB7M%$DkRQ@>*SJ+-$sr%t_-CxRrP@ z8YXQWZGUAt=lZLopMayO@7+f=$kEYGy~~do_sm$&d#KG1U)`LXmN5YCs$F++LS~;I zHYd3^=`+$Rc=WqiM5SN<2IjX@nf#x-ysVXtEa$(dqZNu z^LM!^P{n_h~=4n>*?uv)HC8>K8aUbw}pdS#RjoWC_q-%gHtwAB276;<^8| zj&9hjX8h#wgG&6@IB4GlrZ3$r%mISMp*VnI`j=F9S+C)Qz z$oERNF7nyzyL_j`UjmmhEPpy;57{}fpBR1Fl77w}vyG7zK(^z>lytNmJ_Cnja`INn z`C?=cNaIVSEy|9VXo9f|8@U35?CwVdl-MTP(Nhji>s)|`Gg{RR4H?@z?3X;LFr}r^ z3YdB!dHbboOsD`N1u^MP07mka>iz&h^m2SHml=;;&|jNIlpi@Sjm|{Ovjy`0GNP|K z#$pwOu_B_y3%U%{c#r`=<2iN*m-l!vE^4`+;UiS)Lw5FT(7onyr)%#_-sQw0j|#X3 z`R=}F_}7Q!qsID{H0;3yRpWSPx-qH>2+^Zq<^d3McXbnhA>@evNJMrn4Hh$V1>NXs zv}{Xko#B&)s-jq~&NoVq2YNiLw7PT^fk^be9Y9;bHoz;2q-!JRR;{|%h0|%W1Vlo0=Aic(1&6(&*A5EDC|df_Yh!x6j3Gf`WoRW%K1w5`GU24F$EbswN-S#Cdc; zx{<8>^L08@4^L(~jx0s{e;#p5N*=`D?WD$IrLJanL<{!?R+<(V>qE#`^Z`o~05ti7 z6+K+gRjf(rY(IJC6G*iAnS$idBNcs1hEzzJw1;(VmJ-6U(6T+bb>_zm-?j;^G)N&+ z(c<`UM_A3N@$@zq!6h^Z*&fq#e+5I*@}#2n!O|dnxwu228pLR?QaS48N$1*bu|ohz z6f(zQhQ=#~Ey`UmJZvV4v?e8$JX1dJN)zz*;y|_GhTdIRj1PLqyvzCje9_jDV@Z#} zv*CYRY8mS4>bA|d2qA96tW;ATpHMxCg09^AK#jkQu2$u<5306Thh%U20-B}_V7+ky zUm&w@<|61ZAW!3*W6lRyx5EjtwEuo4${+n)130+fW0~~Yq&+HG z;CI`2ss6zZye%kj-gj?$@E1td)-!iwsc%>eZ@#tt=(c5UzmH|oU%B#kMqo~Ie0)&f zgFj9EY#y`8`$xNVR6k8Wxz7ZDORK(Ya12!)+U+Y($!0sUp_LC5>g}C9S>2IpmwhZ7 zfqM4ewaKX5h;i~cun?57+8ySna{(je_0fpGf+#Z?(b;%CWWkcVyBq7Xu^7Jk@O1&g zeC5AQxx$q9FJ(#tf1Mq{kP??^IuYMirL-wkwfDzGnKVl>Mgpp~M%Cb6F8+89vj zSHDj@^w2dl`yVZY&<0$q6fY?#XGB!kNt2@>Xt?a#rhVIW;EmG|m*qD`nQW;j$4J+!cT z8-DF{5^dlqDOdil!-z>|LVc)7wP=W9HA}^30&xpFkXz`c+&GBOA7!edHB1k#A!~!p zlGVqTWHWbsfDZXHjWr<;Uxn`^>Air)L*#x>$Az=*H+X0_&r~o?T3FbeTHRV^*Mze? zX42(}l35QLk^4i}7P#9z>8mI4Eueeg?#+?qV$sdL+|#2PcMUJ`Iq9zTQ=5hXvDc31Q5PK^LTqtO|Dc6cZ# zw?FSu?@(#vfb+1WmJN@`Li4L)VE=>=QZTkwY223!a@R;YHJK1pF0^>+w%!? zOtFmg@!2(5f)P$IrxYo^w@pq<8AX2kLS((Nj*VAWI5d+7eU8x>cEEAyd)fDlA+4i| zPQaQN!3x=&Pa8m{7=fKaei$}?;N}_sTUHd|{jvlkol8xYc19Wko0Pl;E$vdf1WD*l5?i;N#dF!iS}U?Z+Iz zRuklR2GVt-WfN+@FWoKiy?pudK=A%@gv9ubPg(E6R)t0rnoOo(Fjt$iLGEQGv~ou|t2xdb(V+qN7^jGa!y>c-%;rCwkr)lat^=Ane=6ZI;E}Db`v|^8Hf^{m2z5d&Y}9Dvp0y zTzQ>W;_$GHl?ZNb3W0zusiXY!9(ZRYbzQ-CgZ4oFT%rtyxF~{ zCe`IXj&PdgD(3Ck?P6+GG=|S|xXhG38?SdHb8O-J)h@*umGY^RiSPtNQ_iEk>zlle zQu4+Csk^+3zVJwbME2K zDC}~}Ih>ImuQq**#RYh6fntUAvTJJ$$IgLqD?u8 zT+j0ZLS~7v=jC?-U%BdyJEAwvM1>?>GVihmV&o@FzmH5yTU9>L}kC{(2-Y!Z(CaeE{k4;41eXY z+8?tk`>i97PI2=iyg3HlO)7B9dW3y0-|f&F4{XMJkzU7R@y<#|eiKeF?HDXj zk+3iuEk&~vXejeQF4k8@&hU>3*>@EfFF@)l3Zf{O(+>VNzrZu^MrcazRsv4G##O{5 z_WD1{s2)_KAKp~L>ERz*5y55u;qlMUYLd-*SlgUoN4BV*EnN*0_uyHn4*c?hqy9BM z8Ksh?e1BN?y)kLC^WbuYo_+&wzfh1ZZ*kFRK&LaL_AevK|3OzBfR$uPhJV2L(b+k=aw=>$HlvTT*I+q?@Hc`h0H&8*9ak{uf@_=)hf7~QpWnJZ6 zc|ly8AAy5lNm$cC9_!dZWiPPQlaCA!-r%Y~b$r!hZ!-9~gDBihpn0e8gNOiONave{ zemkFnQi*M|&X{3IQbNL{_e;X0 zPMpO9bwOFIyEza)eP3|fDXqZ z_b->Zju)cqY+zbQ^B99sL*)-d@+P(j1I0>)k=P3qeDZtEc-!$ZTB`>(_OoEmOLN!gyC`o7YDT`qf9 zmhF>E0KTywa~nOI>qk2F^dy0@vA96&mR0hrstlFj<8cEn8c_N&yTaJ($$}Z+)v=Bd zhi6m!zQYFSlUU)p$o9qA5f`XvnHcxzEm>BgKy!VC0m}sDARlJyRbk3kGgc%6?D>X;MmS2*!Ms1 zYbrLpb9JPpZ?}khWk$SA+l)KNERhDht=qaUgAy<1*?0U|-;-f8l>aBZU#v)G+=aST znGRe4+7tBGhh; z=oWAKm8D7>Sh=-UeY*60x;&UZq!1>I6XUKl=~@`0WP%GiRtfHq)cJCtiC^D?vvmxZ z9kfjL$W6mly>F-Sx15%j<&Ks-YOWvC&<=$t(hm5r-;N$i=8zxgO6{04st=Zb@#2NN z7TdNYb;?EqvtVhPS@)0H-0-53(uJeTe=V>*%@o_c94t{9{l)%P!^|fnKl$;8A027z za2u#L^lNvwG+}ihu=KupP|c5xbb9#k*5})kw1*Y#gKbrI7LUrG+ine&j_j|Q25;3D z`x0WC42yiJ1@rSgcjmNAOqhWTJ7js(Q8N7?SY5E#fM!OewQQn{LGvWU0_c$~T-F>$ z7gK&c@#kvJTszO;tnu$@OK{P(H<^NAn>1P<9bEClBgKeXFKF9#etqU3czd))E<}M! zGYOv&u=G7@IC@~Ar7lVj1CrW2a-r-J@~swZDqg%?5#q(EEhbqcWL{cn@>gKt&<3++ z+Mih~zW#${N{~3uTSldN8RyBw03T*_YyG1#=*hp7iZ`oGvN>Pm((s21@3VK#K->s= zP%?7!<-o=aEq*V1fgZzp-*-kF$fkd&r?xG-j_3!1`tLdd5AP#)ZIZ|?DJuh)lbBE1 z%37+H?JLyRWvKLviP*HCODLx~_>TN5z!_$RBs3wjm~@Q6{)ADhtaLf+ojh^^SMt>H z%gx>l;Phf`P~x0ChmPLjt>U{fuUCWU;%j?_hLGi%)Muz<`iJ#j6tetMQ7dgR9yH*w zGx{NaS?)tZ$DEzBb2i8fF5%&b45EuK-leB47FKz2 z%19f$j1PlfqZlY8+>>2kcl^1O?thzm?IL49uB&1UoMu7<6dg2 z_+&NkpwT-u($2-jX0*zyr?>a6$#DI6f%tO6i1>>(vtj&O2WC6(bdp~dtOIq3H)e@? z=*5V`7%bE2v{dOsi)&TWw;Lt!J#F*Mds53g7~x{KKfg{9SP0n)KOL;OB;M0wAo)&C za>whr0;V-D55F(B2LqgD$~TEP{{HpC%GkuH+?iF=6{^f4@X z$UC(&6nEGZZPlOd^Cmz4#q6v#*!y{(yBMHZ_b6X{q`oS)sZU~Uq{`p;-a-7+zqI@W ze~0j8)5;0eMmY=7hIT-}WnBYK6lE0OaXk~Vbi@d)*?k_gu(x|1E%DCYCnbI=z*7)B>fUvLDT^qNz!v6i&;P zxELH4#R#mtg-V~^a~byyIwlj0*R>A*8rn4Es}OVCX1ZXc-RPH6N?aB}Xw^YW_z?t0r&G@i?f)YrTC`v78u*vkCrvWX|PV)N;w4%b%?(@g^nevQzkl013~qHH zmFF{*H<0{axVwr4_0OPjx5jZL5BaTZAi^vNg99XU^v!+#mK+b5PQ8({Ju*qd~&d`!Vbb+TBY`>DD zarScP{CYFA{+xJh9w8t+D`PYc&xx-YbF|W2+-F67-tsKmG*ZGib#XX!+0;_X(Twz) z4{LK;0>9E@-Bnfp3iga*x@P+-+I-wfOXMJR(mh|TuDoC@7lz*7ceoqJcu|(ISZ#vb zLYVxQm?M*w&-jZS7Jrd&t&|^S!;Tv(TeI7QW*}_MLw>;3d5KWw{25Vz@Cf} zaPBQX5GZrXu-03!qlSox7z>P>Gxv+PijsT$DPX0UdH2Cc@yFPl6lTLAn!9^S=s+IisD&;H(@Uz)}{ zhTNO1{xjoRgFbSL@F!XdegoLB>_g6riArfEE581@r_W>Pcsu6$`qrkqn~L?{92^6O z_rEj9Jc}j2as$=)QuL?oN8Q8y3xVQ~(KAsWa`>d66?YmG%gWt0M(B;W(un^VXf9*v z4DG8YIF0EAEO9y3Ci4*gtBj~VGZ}Gn>E6n~$AN`&iUUQVA-+rZ5@i37wq!{~KZsI; ztVk*4RNxun_fI(?zx%~YP2sMgSr6dwnW)09cW4EU;A#%y~<-t*vfRVa!Ob3Gpl%csV^; z&(V5fpo>rp$_^xq zT=OcuMw4_IPT$gdts;&3#xv{3k_aM=w6z@Z{(S+zb0m0>iUi@FGFj2r{htTFIBo{w ze(m3ijF|txgT_`b9zrg4M4Vz453HWz zG!SL|27i^VUMg78NR9Vyc#dgWZ_r*`Bz~&}KJn*Nq?lt;F`9W_1qjkgTQLabv>aoH zzt`{@U)I^7sjU_ZxiB5ti!a8z^t43J)?`ZVErj%3Z>jAkb1e7bA~vpQaa&s62r{4LzfC4$wzYHC zm1oj`?vNs6MP1{+Jyp;woUrC6n=}vOb2+HBqY?#+!cB4LRbNg*@cKcZ5=}9iY*c*h z3$Q)+x14H!Fa7}wxO$UGoN-wB!qdl6}~?npDlp1ypULq=KIK{V?<(;q4+tlBAM=D4ICH@B92TJ`KAM|xRKEi|ILykL#zI@SlaM~T& z+_c%zQXt;U_5SctNUwJ)d~J_C*Zz~g;F4p1SBvA}NPy8-a!`q&dI}Rj+;GNkNhsF) zKG@Ib9rICOGp_B)ePP^t^Jx8#iJoBTeHSOXt)mxPd)ej%X7hBUzTQsL;)7CB(rb^G zM5KFp!Y&%S6mIL!9r+9R7Ro{b3gZ=eQu7yv_N?;@+I#xUSlLLG=vyfX3i*Npj(WZA zowi5AqoUQBw6)m*%=hi|v=3XY#tm{MhbkB)Lh=`LJxV7kgh3^UPaF*%W}8pb(a|P% zZztW~`?t7xetYRhm6ZWVO>gd4!F>ZCZwJ4bZ+ou|UPK2tlj>-(F=MEH1+W|RYQOEY zwF}&v5Ds5h-Rd*X)t_73Nf)*^Ei8Ad-EX}eMO6uTS1q+!!!mZ|}*U$~Z&%iu?xY?MnW{o(!H literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/aerogramme.svg b/doc/talks/2022-06-23-stack/assets/aerogramme.svg new file mode 100644 index 00000000..0c1ee127 --- /dev/null +++ b/doc/talks/2022-06-23-stack/assets/aerogramme.svg @@ -0,0 +1,1241 @@ + + + + + + K2V APIS3 APIAerogramme + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +IMAPIMAPIMAPMessageindexMessagebodies diff --git a/doc/talks/2022-06-23-stack/assets/aerogramme_components1.drawio.pdf b/doc/talks/2022-06-23-stack/assets/aerogramme_components1.drawio.pdf new file mode 100644 index 00000000..fd9e6a62 --- /dev/null +++ b/doc/talks/2022-06-23-stack/assets/aerogramme_components1.drawio.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9198d0cfc0e04a56f84a353dd660b294ecf0dc68fe429892c8e9e604de016748 +size 31966 diff --git a/doc/talks/2022-06-23-stack/assets/aerogramme_components1.png b/doc/talks/2022-06-23-stack/assets/aerogramme_components1.png new file mode 100644 index 0000000000000000000000000000000000000000..fb81b4609151abd82f78ddf1eed137f86a1dc25c GIT binary patch literal 26898 zcmce-XH=72*EK3VbdbVy@?f2DI!%+ z5GescI($3c_w&5(IA@G+oL}cpNVxXi*RE@=Ip^9LrpCI|6l@e1E?l72LusQgTp&b( z?_&^B@Y%;S)Oq0o>2w&ErKlK|ogPpKk&(Qr>}~VFI$+0x~i#K|vDkJ}zD% zE&-tuS3SbOzrgc=KzAQc9}o9`%E(B`$cRhHiOVQiNJ|OG!e!;bhm4$rw4$`#KjmG# zJg)xRpuB_>Xh7ITT3Y0vXAvGDp+13E|7l8EQ9@2a;hzT{VJ=?(cET2pQ3wn}d&mdb zxP^qH#cjjg{wWg{73A?x8F!BeA2$#De`S$iDF5~uyvpa_UK+V7n<^P1t&wh)<_a#l zx(05#!T%JOwz}%+CLbz?G?foA)pu7khueBbn!5NR72LyuLj8?`0u&=W0+pho4B$a} zddg-3GCBsv;KS3(T{kiip$EE#(e+ifvkvgH0aYTbqQd08bOmG(QPGy}2>(z&ZC@EL z1vh=^KqDn>3op>oNEFHoEfpXWWM&~R8=_^2(bLzl4zaa~Hug3R4zzMHvXS>enoGGL zqin7QTU!_g`D*)lSjub5*ul|yo<0gD*0KmNjR-p%|0weS{{XnPUzoWy=u&8?Pk^0; zxt~RZTU4|%TF1vTAS%ks)<4R~&dAPG+9EI*iAG`Ek)STh749mBzG@U6z=?qU#P zW~At2>ER6?dFZ-GS?dRQg=*<7; zxGSJ-t^B-9jFCQ3RyIme#^(Ck;o9cGMsVw>C{smQHv>zIlAlLlfS0+ir>U#Av6UPg z6=q-rnh(^`3AYXOwhZ@k(KQM3kM^>O2=KQxl$O=g*7q>C4l!|)m+=m;43u*9Ff;)D z_LjGE(e;iDmqY35gu$Z?ye;5*s7OmsxHVEIKnWgZD;*98#Uecd;c_N$;~?!Iu=t^J zVD}@;^~??IWZ+VP=&PpqBEANuW?F`_<}OjTN^Sw6#vTZR$RGtXV{2JUl#H!~A0pDt z(=F1&CCUqw_6&0iMazO#%&fh%wWNcM70lcbMj;^qa-r^+U@dvufH1{Kf4y*b!$5f@ zlfXbFgo~Las0a55^RuU?3) zR!}g;#ZO)y?d|Ia5BKoEL`6y=l@L;{ZgM)&mL?b*%ODwJLsL(0Gi6;@&=Vi9jjXk! z!8bYZB0D)RZ%l|1D%3nQT3HX}9j<4JGBiU3q5PyRP0hlUjkK(E;FcI`F9h7%)dp!C z<>ML-*R!$p40G2GSCR)UxP)667%R&L1^LQHhx=M9YulN6x`5VYTy!EVWXywueBF%# z-K7k?t!yx!O6IoaQf6pbTQm~mucx4A>ZTK6Y-po^vX;XbN$I2Y!U6)rf#IDO746zB3m$h)U3=VP$_EPe)HSpJs)Haa`w3Lxm)YEpc z(|2{(mWuF?G68)HH87PoMVZKldB_>KD;foQhJXWtaFa*b>d6M`21{FjU%^IbJKZQZ zbLqfH8wGRxV+9Yl2xW6uYjas+Ph|x;q%yFLI?C?KrUqb15h1!(TAm2-#mL4MVJ@d@ zV&bbKXC)V+1^%TK0C$zQ)iFf}A{2CGeXn|6b@$W-9YF+2%jy_=AyA4@(O!nip*ps{ zdaf8N@W4CLP6_;xik7kP_KZ*t!}l*lE5gt`z%ATD-%>ja9C00eGkFCM8&7jzS>vyqQZinO;1?+9VWI6Cq!DO;IY$QUYnqogg7?%tjX zK?+*N{`i@dHP-VpQAF$7=m+Xc`D*)K^{~)$MOhdGUeyhWiqa1?R*G^r(zA4NRSphD zz!l}?f&+c^b#2^y^zaMoFBk1;6QGPVLPY4h2V3|fePyhbu4;$L`6~zM$O6lVfCmS7 zT1s15>4nN8l~HyPei(PXV9y}K0JCs&Q>ze9V|PWDkT9f&m5o7|yRx~Ww}rNwydE&z z2sd{)O2!=>9_1Tiq8Fws1Gm);17=<)1TL?IP>hf_4X`r+n+`u#@F}BYA{7A)J^1w> zTmRSagWvzUFj&wD@Yk&TE7{8> zp&x8RNHYpUZ-EwA0#}W!g(dE_codpili1jJ>t2}O6gUi$} zqC}RcNA_%XC88DYM)r`Ff0~myr(u&D-fQZmHPcL=(JNp`pg~)-2D#|w?YFIz-A_0_ zo&3}6J=-20w%C{2*lhkzP3VIL>*Tp(a^-wA_xI$K1!uu678!SlT6L$?#`#e5=_L&e zQ4oa)1sCFD6AT7{5GE(Wa8LpsB+U!r>U`en?Eb|cg8dA{#02o32?#`_spY6o7sG>G zyzj?a`?rE8PmeP4B3za-{--q~s91;6Q-f>L(Sa%nbUiUTr~-}Q*Rr75e_LM=CdNXQ z^-IlYbyUS%?zyv8{_{k$2O=d>IAz<$l!9r>8?h-4h=~{rg}mW8$gnU}ZzsQ6~C<{`X|RxsIr2*FlQu z7C-AAx-yM~G85@I+(J^wm4=QTiqoTC4OnJ@<+o*Omvh+U--NT(g=n2SZI5W2SKV@q z&M+=lH);#{zOxrx%S}k!QBZlw`ibe}6Nf|5d=_cf(U)6Y21~EN=GD+6X@Pu2}Zd^6R2n=icNb1(P!O+16k_aoZ-WRf8jEkjBS< zrpOqfDKe&VyK41A>E*DkIkmoU_W6YKPMPO0WEw5^59ZhRb@L#UaW5k_yyv^(uJh^A ze2>`8R4QCp=uI(9|2CBK>-X=S#5Ra#0;`PsJyL2m)3471L>xOXa-Q#4>|REQCxz8t zPdGbF+nnn(&@1H6Xi`C49?qt(BH36TDW69+N|9Zo7?)(wuO9;iEVA`b0g85ac&QKv_H_Xo3>f0w$%9qqats^*XpQ&+7Y?ftqg z?WS{du#|TgcnKQF#dK+yrG{!gZlAOB@Wr+o1u$3X--!#V_h6uL9-j!aC ze8`<}QXr{TChy18oBPnD{DZ>NxZ{XFa*mzRH(oRPvymudU7mDi2w(eLF`|B)eRg&n zSRODCmtCMoAHV-zalTQJDfEK-2M58afV&C-7^-~MNXCfz@n8-zX&t| zk6c5{pV%uuH-D#i^2FelM6*jjseYNNa?G?;#4uUR&c1T6h{|kSINO_alJj^2{WIlUkUB{w$r{_zPn06L4@`!XG-2-r5mC!JD1&f5eW&)M zl{^1^USt{#XC;_&`aa>YJ%>tda#5>CET_1BTEowp7&?`^QZ9sViP6oT@7e6y!$bnU zG^ILOY<2E)l_~`%U!7^C-@wpO{G98&JYH{|*B<_BXs*HI?M>Uh3w=q@q>f}L+*&Pq z_g;x|XnTjaDWR&S&!>kTu8ECyBV}r~<*n{aRS+!OGWj2AuMZr8+>J!n2}vj#ORMyX zhLD+_BV`-UaxlEe?vowL92V(hCULugD(@ffis{{b=Q@^uA*d7cs>|*cKwo76gLAIvYqCBT6Mv6gnyUfm2G>Z<{7&>q` z1?^(^08r65-8FX{Hm^eUI*kn%cz>>;k1;s?xfYe&K5d=8AMLg3uHD9^3#bM@}e@S8tI z%15{cz|L!;0=;ASx^{ZJZ+qT|oD!nwvu>osMr_Amz2O!M@Q-Rp7;V=y${kJ2o1?05 z{rqgd`?`8u^c`t8(wL{3pJzNWxi!=sIxqzuI=!!S-dq>?@}cu}X8b7EG_sdNHS`Bn z5wA0my(ZL5C2X}vrq_TphSWG(SQ7UAb^l*WJVl(8s7KFaEcOt?nxxuejzuy`<~DSn z-Rabhj#|o5yzQO{bt!l?zbVlEoY(E=d-LVmKn%GfW|1Vm0(L!UV_N>I<32P=Y>}i` z{K|94Y_ud~*yShELjDb?YMkGFWTBp5<|T;xaf z4Co0758pxz2d#asUEka0_p8*-xJ~?%T|hG#c3fv!uQbN4Jzv_xx0@3@QD>R;N(_m2 zLP5+y(e!TQ&XDDEicP^(VeAL2eeN*M9Ck%d)u*lfae0#V zrHLJXrv2y}bDumOe%)YJz>6uy$>$-tv!cV|xGzU!|CY@!N@^s5WM3Akv#7lo!=Zmm z;=RVJ+xb_Lm`yAKzD45Di&>myBpstT##~jX$Pmv$26T}`18gp@2rVWF``e*z&XF)h zN+vPqB+U}%OQ;Xmy66fCdq*iFWO(->;hH-IFPDrnHDlwR(oc0XR1G@_#@h4@Ir|A8 zK;ZFo9gTDDByD6o#7x3VLpk@0giM-plKBalOQja?6FWL$Z-oN6Oxv%Ey3?7Yt!v8_F+h`0g^hJ^OsO~fT%7D3dI$nYIPCCRZ9 zniSH{Yo@hrsGW9(tCJPUCE1r-W;&uiO;1PWhry&Ep>Sr{_u&kQ+`x&krIVDTrA#$q59|SWY!uU(fek ztR;U5uSM0?hu)hjf<||OSSXe+U@tf|!MIUe$Zw+Ql&Vt0#TVe2xlrV!)$&*(R4xAG z_Uzd~N`uROXr}63&U3pLSKq&`E>AdXtM>tGa)Zx_IOF*R$Wqda9`n1n+dQnyTp6Eh z&41@!rsD^Ws7=W#e+xHYxTA=*&ly^wQ}!23 zL@w6CndtTVZ#)0mbdyIYe!TYADMx5(`s^2TlJd&vH+)vz~&;s18qM8eF+5lKh^(+j?OSbL?#vSr#hpIw86SN^lkVx055-pO6uSp&*x-sbF zK*^rzKV>GA8QFLsc?ngoGUBmc%-Ad-gF*H%qpN&R953IX$ko*xf5JhFeNQSJ{j9jp zkjZ-7k$-I39hq8r+V|Uf|Ja2xk`@>r`EJmH$ z=w#^zsX)OuL}!NBEu{MI1wUnu5w+d*H0dlBXsPQQ-jsbN2kncu%;CZElC;zZu?L#= zQl$(HI5zG~gr{4|cV#HlPbOVQ|O)pPF( zr~h2wIU3%CsQ%{&&GkX6wGi@uo3R^r#6pW7RM7pY z_od>2iqtrye#N9vc(TYz{BboA$eLTPReV-u)xs=@oQ6k+l+@eap@4VS4bc&rAwH&7 zZ_l-dXQ8W5aA5GOzC82N&94R~RW9_0VSDfr=>z>6nT}E48IM4?LcTGW$(YsTHDY{U zC#SL!nWcky67~CsT;QDQ(aG--kajkEyee196Uq{9@o16y&bG_91kS7TNAFY@`Ht6(-I4dRmpT5V zv+UIu5YLiS%s6d2_BE67lJ`(HYuMRIlzu*pp!59fST1bURKG;&DdV+;!rt7_G4usd z$bDDIXjw!i^g>H0qf>V8nwA=LfF{%GSfKmzmnt@6@xAeE>c{VY!97$$V)NfBVqHo0__i1lHtXdQ4#sfSZf-FEvZB~`UDnIs(z z%j5pAYBUsa(6OCR$+Q(!+P8%uSD%51-K>!J1a~*EYw@YG3t3cFF2EG?oiU0GeEGs9 zSemBi@u$&*K4KtNhJOGpKHIMYfw_TD z{se;$Bodq#+z6iMf167xr_^Y@3#>pnWA!-J(zq&&0q#Oe|45Btx-iD?_d1$<%qn+|`1 zmfYT49lRAK@p5GS;jGjR>+u>(6ul}9<|(iJwPa>hCOxs6WvH5~Et&UtbhN z9?;dNi_x2S!SvW7!mEncK*VQ>d(wxT&)%5Wtd_DAZ|GqEvt*xSw`^J0wzQ47LCe+|nWK zm1|_=;V!RkTQ@4k#`aKRORey8!8>Z#rOmkH5(xa;AOY3lci<#x8G23B53z&r%BU|*9?Cx% zUF0$CyM%E)OV@5eI!tlciiNk(3dK@rk~5*-u7@`Hi$fP;Pb=^KqPdwBYj@j4+oXV$ zM-7ghcRIB+&QD|$h!;E>iYo0|e78#wy{`z=Q42=N8%un|%IPpA*ec845hEwsyTY~Bis1{@^p#hH!TTwD$Cs^y#f%E^+9RNs1~2E))(@w zv!qxPDqk25htnh}qUD@>F8rbDHAqgSlEh$)cT*X%nEOyDOoXI2yHmz+1w11Y%A-ME zML;PS!Vcr^P*V{>(9j(9GuM^LQCBvG|FW9yihIdZ1>q-^fmK7aX~=)TgnA%ccVZ8B zwlUJiSZ???Qyg0Jn-!kcQ-7h|A_1${eVKbbidCJ<|an7i=2GM3w)^_nw$ zbNUOaH}3VJWD&>Ip>>e5v1Kzw@2T16n@Yi`q)14w!j%NSQ$< zi09F@CHW?gw?k3d!&&`oDaxtJlue6{9q*AQr)1y^s#A!_eDDNNvj;#6@#JVPFim?; zskt1<)K&y!L=eLe9^R8)!s>FsFwnlN%E$b4-0dLd=YE-XQ?qIzcr28o@;CGp*s@sr z3n=oL_01}XF8RHYo$qgTR~bTj8B(96#|j{aIg^h$B}rNjpZ2V+K$9Rmk1&9Nm}9(# z9ee8bs>zxnOJ!|QAaMnLoz6~cTRW4kUDKG*kr(oG(T}rCaW{vVWk*^{ClZaVvV_J( zvOBsL2^zT~{ItzlF7X`|Dr9SEA_H$%-;Ttg`i7hXVM_-Z>GbRLjSfNpE@{byTI>poqu^(Nq%ZSHbJMx@=@PHAaH9`6%Ti`_T0$wShCkWw ze%6Q>CH0fwja@a)Ok~Fq>pi<-_LO<@vi5PB_UQV&A2%9he&Z^^SGcXE3jqiSTqUA2>MQp4Rw)p81_18W62(%gp zdY6NZ*kS{TGV|Vb@s+F_a<0oKjkr3#-R|_;Wg)1)P6W%^XfPryN#FCVt`mF1^_1n2 z&M|L>B+-w z(_Q(Z2S@p$I_@bmURa>Phs>rmxmqn{H(rT0rP2^Qy*26*;BHc>$Tt=ie+6KteBPC> zDSSFLzMt#%I0!PSc%6v{90{+DUMJNKeAsjKt1)P*?fBhuUmD3?%N)BFM@)s#{T$E` zbIkO$)%Ru5vIf4EF$LOw&*b<|NvR1emuT-?_}KWEjxOfujduIK%>RNN9;By_F!r>L zO$&mg@U>o4n7fzEN}7`eu+{7TVyjfVsgyM^s`zO=&Vt}G!o`rk*e2yR^V)|2=ydvT zYkvm+0<8bPF`p^q_!Quy{xvQxFw6>ys!No7pj$5AF?2@%#jFq!83Qezb$s`Q(WWmc zK=&EnNqO6^50>dOb$&4q^wfLoD+v^nLVz?yL@e+VQnO4Rp#dhudFkga7~6WS%l{SJ zcDh`#I}}J$b>}7~wYa@uX=53)J)Lg>5h1LL)>0E$Oi7FwFuk5)*}|+URwW#}J~+W= zi3hwemYHThJ)-b^BZBllH%mfzGK>$&*Nq`Rzfm&EH!+z;9#ns`9~ z`NEENecEI8CGat?T+APT-!DxM{04M$IVzG#C_feo7}Ug{9-V|X{(dnRorPNmR>D3! zjfxe4=gUCoJO(&3VIZ~11uba$j#Z|PKh(QVv_R#$(H_fW6<$_9@!R%7 z3Rh!$EGe}t|9cjArmCP_)>mo%Xexc7u{JtOl~5rhfWDPtwoRjeoOZ~I*qkZ8DSkzV zLC83)pfZJw77f&wpI~{a0md@BBj?kqjvB*9XKV`oEawS8!La~@7alT)sL3flzLte(D^NKJm0lLX1Kb$QbN??0z~idKU}ZesXamU# zuX(fqZ>5KW;#HK7)g~q&Aj>L*E~eGIz-+sUTGW`DHdm`S0_2u7FOx$d(v&Tl%$vTZ zYW!zZNa~}db(k3?~z3L*b=p8j7~o9FmpgW66{3W!is51Q5#8{2l{oByfv*WGnO2MvzCLjb`!4;R8W9d^M)GShozMpFr zWiQnIsj&lZ8xd_BCjunX1Bvof%hHv)y;!E6dt~6P@O~;i0Phim&rVgy!wQi!H7X;PzesouU?X-8U7ABoYhKiR$U6&WI-}aQ6;`#SqUTf@(PE4N0f6Im{9@9wj_5EG{H#1TfhbkOyhQ*JwD|c*a!n>Hj~yE(eD6 z=9?BjtbAlz|M~VA;rJDcs#O4EA6V3)#{dsg15{E8G+rwk8_Hq#0aSy;V({cNK-79~ zZ%MAqdTzcP$!4usv9fq2StSgemTI8oLH=;z&MdP8N43**n8T9W>eh&u{mD;jAoL+DX0&_YX;hKp4a1s=-xE%a@qyuo$?}`d&3?op$@WdhVl30rPmQVzdB+EVAIaxs z$dMy~pbC;35_;t6Hc5ooM@p27OvH7# z^dH$gU9E5E`j+s^<9`(5Tjft*eEUx>z%Q@&cNK+BIJ{=vKa_moHe6}Zq54Ee+V6q_RN`I+bK@X(l}+)$!z`*Hy1~W#ujA+Z&F3D z4yMzmyutxx*%SyFKVNYtQ~|A3xTv!G>?q5`aLDJi5*gXcn^gyCiRMDZYpoW)$VGNW zy4H8D17zdkFE$dpLaf-&jd>Y zG7BpDr?bl}USoaLWQP*fA4`a8AEM(?d{%L=z@Tr#WFE~yb8j3hqcL9QaZJI{dh9(H~fr{2cK-1<#I7K4jFSR}(s zyzfgd-|9NvynM8fLMseUCTvL@bjWLCPjeSgb%2Fr0*ZEC(U0IG`L+HOh~y~P zjrzcGJOq55)KA~hdj!-RO8GzwwqT|mkNzgIGy8mf7mVk!&)Z1bzk6X`Kl2@a2z@-(Js=(sjO;=(yc zln+j>mr3gHg^C-WanB{XM$wa@Z8aF!A`v0TY22=d^)$bL*h5n>pclU zV-*Oe@rD829_ABY(DyR&n{g)YH(vL?wYWL^^17fA+aI>X12MqB;?omej?cUWhHPpn z@s=b)4E>oN3}LgZa#%bCDqwg!UoC!=6LkqSMk(3RQ$-4Zl*I1l>|H(j4g`pHek^0I zYo*a_=MDf$+5AI^vhq48wv9RA*1ASLr+R3a z5ls)?Dqdem4vc7pNBH5pzQOW%;49Dg?T+~xRT^EK(;&dV}F{@M=9_jbNB-3r)k*UL*IYF*F4)z+tc#;CGBMXtqL{gclUx zibX5`JOX>JEZY2r9X{SO-7Q z$Q1TMR#)Muf*UOit^Rm}>9}z5a58!P`|m)DAF2XX!yGf>^Y#BV;7uJOWc>H?D-#!< zo%H{;ueLP~!|11*B@##U!dZvXzx{w;FQ(`lUFzU@J8Ksmjx7t8Q`Z*Bt+YDSA?y)cT! z3|Z>&9AX*C{F~1MzN*W9)_4~Y@DCQXzNcFB?y>H3?NztqhZ~nt ze`oxALrqA5=*UTjO>w21&m8}iy6~+(oy{)ab?!7dH}ULHipUV_yj7^w7qs|=Oih9bQ$^0=Hl*mzin%Er(^Q8qnj`!o@(dRnM!hZ z@1=P_L`D--B&EqAQOnL@+&2Z`H_+F9a;u2#44zYWm>1ncvKpXFYIxT&m%L-PWG@AUY7uk zTwM`M^4lz}Y{Ov>p4qhWPKUesFi~$T^%-+bKN)9n?D$oV?Re$<%4;{+#-6@4q3y}8 zQ$5>|e!(DgmE&za+d*8!rYy*So4g)QY$3oP630r-wLMMAWJqEY9k`-j7HC2M&t zEH49}#(yZ+DAz0rQYD|Wc6*h(_lpaau>35S>5)JNaG~?sejV$Y{Y7g=4| zN&^4uTZ_m)P>(lvGSJmV8@$R1XWK$I-aF-f^wKx^E_y4ppfc|A{->GOp|t@o6y%!D zg{LFJ{H|A_&Rxzw5D8CEKJ^}*N}?b5t(_SbB7fS?Di_vA_ohIIY|m#tt*bSt;C0lj zV0cZ=gob{BKeE=mJU7gFlAt~M=cP4e?UxUAe@`vUHwYz^s+Xwcs#(A2erc4_PFtB( z9;e4D>s9q3?y}jN&mxD1e9W}6+rD4#J$`xtv0<6mzYR~uFO zM9sZwb0t>%YklzQWq&ZR=^ohd_bKtJ3u zxLCyp()424LaU8SZ_g|=2ViQgp4h)Gt6UvQ{S#y-+r^a0B>rY;025rd5BA965rx0e ztC6saX(?3FNkMD-K!dW$1^UPD*Rx7b+W_*4%^_7q3Jd68zehsJgiCF!Be|ATZ{3)~ zB?-@FCYif9qd|HCU*6S#1(coT%}3QQ{n|S9VW_SoN?To?_)1JpcsOt#texoS^osU& z&y_!(-&cUqFFg*c^jUg1_CxD#8#^}ndEhiW;pB`gZE6>1JMz3!*t)C4qBA~R^iC(s zdk5*Xv!XqXlkAt(kn{6zC(NIpn8o;hX%iUtLn|g9brw|Koecaq#p*W2C$G)Nf9`9u z%Q}~KH7+#E_h#*&yMS4twxKUz0kq#w%ReBIWf~ozFY;cgdlEQ2VEmSIk)89s|As@+D{61560Y7Vz=o^HR8%h_gLPA zi*{Hi-HG&vTmG`B2>A74?7FmhF%@Fy&I7-vLXR9jMTeb?1`8tNTGZJsuNt1}>^D!Ye;9#WTza}B7FA&48)H{T9)42|H7`*dOIN{c?P?qu%0zLlKMn#(ZX}T{x^Utsq>)Au!fBkHqM)9Lg$45veSLEGVgnZndpNzFw@OfyQp9EW6Acyf`DKG}Sfy=G0ghPQ96H~oLaAhyQ6@Ghi`~2**{qlijZ^aDD9WQRN;7ZI z{#j#^rp}fj48J?+A5l;_&wXwz5PHiMkq>hrWSjOAE_(5VsJ$IEHfQn&oqb#S_Hb{# z^>Z}qT0@kT=IvR7Fl8i6?mk&=#mr%-*{SE=VZqDD%@7UA!T5|$`S7=s_rHI=Id;Wo z-WA=9>F3qoix5?bz_A!{E0*5$ckCP4^PRj)!6=FP66#gWCgArmiSoFO8cO$2;`H*T zVmG_1(*~00!8Jzjp#9$6)v-W1|8G(%9a~bTVaGys_VpLT{l-h5bDs|g#6Re}@g;>( z(b4|so*jeA?jiiSB;D)5Q1ypocf3U2Ubba~L;8Sm&rJNvm z^vvdRoB-0B?3a@9*R)&QzW`U>a@a}Q_kj6?o4eWOd^`TPkcl~}Ao#f_i^xaqov3|Q zrhpE2&1;Pdyj|@eMS9Yb7B4sB@p9l5-E`^-0sEs&!V&5|?WE{TKyLkb<3SjF;pMAa zDMDy$z0mIEc1^1wDV||Y2#qR27$>nHO4(`*x2t4lej-dzJXW-kZ%)j-1~k03Qh=m? zTT@7kgac(wlz>~9ZUE9vbO=|DKT)!LWHW~RtMP-hV~>U#zQKxA`Bu)faY-CJE_7s~ z$*W%;*GO z51O!)BNLf_-k5Z(aHuYG%)9u-d|6LpP8I2K>1KrtTLq$R=Q|_Yc!FG9=n}hrv8aH< z**6*SukD|anG>6~S{WCGTut657gPHDX|g@B4-{XvYMhF>E@zAOb?Bzn_~l3!5d-JD z`upK~Hei#Wu4p;boZsHND2SnOw?i_~Vr83#$OGW?=a_dRW^HxGAsmS*+t#z#D?I03LihPWZ+j4{`g zNfm+1Z)KiIBN?H0T!+lw>#hSiNTo9`oQ6Ns zO_50z8Qtsg0(RMN;!#F~JuVpz@;9ORysaU7O0rUj+f@8-OQF5v6f)O0ms`W1{ttN{IRe_o zVwCw!Al+o6CCGZDSIv84CtNOglXz{aswfM(I1giOVHa^B z<|UCxeMTPs!mZh-dE^r}`|m1iU(2tE*@Si&rVLS6VYZjd_N~;s`_J}f#86|Ish7NS znb7N#wcS5{#^iPypE2D#KL3`a&$up8mc>I^NCGbhA=G8Rw{CUZ@gz#t`Liq%qZkSKK;Ad1k&?)?4K;Dc;vJ z`>|eJt5bW2!xfQhcqsbazN$vM?Hfnk(ejkSpN`cBfz|QXHTldtul=E56A{hdcDU1p zX}{T6awEcjPOnq0uqou2BDo{~_1)^4*zp~U78`lrnf8L4%oaC{xzv!QcOyItiCt6Z zSa!;#6y6>00|A~xK9Oz~pw{M+%FL3f9Mfqb_7|THDwf(LRILm23c3W&tGVMVDM&7T z{Fal|GO&5Rv1(pMY=`7QcHQ$hZ9lk&tRqz2Z$&@`Rzd!a+fI|)D;k>~jV#Ys`K~it z7yTM2C~EdTMdVd^_u(=u9=iGnnoKr0u9Vc)-+F@n#-CBU-{@#@FNlDNVacI8w)1U~ zg@Tir(Ay{aZmOevnoPZ^RGW9C51o5!(Cxh3KM5YA=Rn@plCPgqMf z^)4yTITpn?Ps{Cuvlz_f{OWb#^v1|aZr=g{z`D*mG5f5>l-JS(t~{^4;<>$ZjTth> zhb;)YNiSp_NMD>+Uay%=H3GY|PH0_Ui_)RoVR@r4mlseoHACEdg!qYk2v}(Wb+)w~j>JucYoB(xVbUvxdc1#Lk7GiP0BcRK6%RgODfPTb+Z^ z!RRjJ(}5Y4AZ@WcWRLa!Wg&{y zMKM{LS5+N;6YOBc;#t%$F6R@caoV6&`7Y3Md|OI#{7rvVsUEL$l?1RNG9T4CRgoo9 zxp*}gV}sO78jyd5x0ZzaYgZi9?+>wNMo{q~?Sy~L*L3wDZ7WRgZ^tRgSECq3EMGFB zt1@fU03XwoJD1N_8lBjf*KRL+~7*89d^ zj4loM%wED4P3H*Lxg%Q2=w%K+T?3Skm!YSc8;~8=j$aUkTlV?E2}BNf_MJDpT_QtQ^M!wBdaYSiHys;7sgsS5ucmld6}^)GRE)oVo9iX55mNU+~lC#})0X zL~dHfbepCF2FD`p8c5|KW6rsF>wS!kZJqe<=y1-)a!uq$IXK!)Rva!70YPoQo)qXX)**nN7uTvg&Rc9tXf z`7eT!%w)zFUBr!D|dt2|#!*-VS>hQ^P>Fp64 zZxnl$5*?wDCiD{8sk-Ej)qwrUtQNj-$;zRDaFy4e3!E8+x<3K;j7#0?me7y?CF?g# zi+C48cUR524%m1kN|y6RZGV1z;4s=-Pq;-Hl9x|(UD%9wa*KBb3bGypp^}cOU6fQ& znji`nJq1^_TA}R9TKL?<&}(=>l}mgX*0ut5$?d%_7rtik=8k-Qe&q$hw6$nPO=g$h zH9!E+GK?r!9`-L$dHt{cmXfWWS%EEtCM{3_k@u#U?YF{ZAguA*duI-K%(zV5gA*~6 zgP4b#nq5JR&?R$SHt7$L@kSl@ugbeSbK=0@JTA>Q=fA*PiF1ce~m6?y%(NK(LF%y&mB7_KL z342&RB>$exQ&%%^Z_OL{CH%F>4`c8?`3{|_f@`rH96_s&mApvY1(#nZ4-9T$E+<$h zBf)(>7`9V&uK|&{HeRo*9I^~+u4c|AaTLeYkG|+SHdI9py(b^vd(+BQ`W3IgfXRQ0 z!a`qf%6i#0C)_8O7uhkl*yepo6k&r#fjsTZ$N7$ zxa#%>%%OyCa!)CFkb=qF$xr@je{|^_9?bs_`S^b}*_Wc;*Hw@-tu@6_K1IFmB(99v zMf5)zB0d)0ftM4>OML1^ z4Bf~0Ai7cegeTW>O2T>zCZ?<`UtOviwwZZ6ai>&oESK}Ly(5o3A*Bp9;1qq9miUh| zd#>|_Sq!kY;&UQOhMy;0717wz`(uK3$10fGKqKPplCTL57wuXLd{Z>jyG}YU3wTRl zk5|ZD*t<3v`^Z0`%aEFQ*D6ct#`hg0uHaRa7Qe~dJakVmZL0#QBR?iRNR^o4NkQ)y z3z$UfBbKz-wjzxg3deWGIoXS1T%qk`JBmzdy z7|wfjotS{(JPJVDyMTDH)!tGF(Yj@r=|ya@cP-jVa=#9GYGW`ZfVp2o~qe< zjJGS7#CohXZRC02yfL_r^8Ic0B00jF$sOY5w76Ao-SF|_1B1hv$M}TV7Rg~Bq9nqf z#o!Vp{!;b^bL0v*1i32VYuY_=;36nJiheD8Ypyg%kmI1hRYTlWETcgmJy@hVF-q!Jyyq$phlG5JL)`o8l2)7n{vMfJ98UjziC zVGseS0fbRPx|<=ThL8{}8Yw}#Q-=;glpF>r2P8!)X#}N1NfD$Q1S#G3!vB8uvyXl3 z_rpHkPx@i8m^JrW_qwj@JkK8><9#eU>guoeSoY14Oy7y`B?@f@eK;6}T>&}gzJssL zmH0_vb$gQy;D1N2f(Q9Il-q1-{R1}O-Y9R(-7${93-E|Q zpk=Cp43jj7rEE6YGI<@q=s*e;yc#t@4)y!0)eYy*LQz#{4!L)PS~F0OG&DY|rV-SQ8+Wmk%+CTzBr__k}z@h%7^H6N;g34;a!ieQuW2}6tP)-`RAq5;|Z&tpTEjly9J5!luV zeG8e4^IGZMl#5)L zp;bMxt(R=*DHjvULWUPn&#F`u2kJo9Z{tgedWrB;J*0QUo9o<@ltyIFo=R9}MWJC9 z?;b^Xq3@+#rpfW;pl_GXSeizj>x{6p}u@IEM9c`J@T@x+=`F^{yjV!qe4p`a`p zJJNW}`|b|mOBhnlklKoPB0E3)0$#x8Xp_xK`5ikZ@15g@ib3V;sD4@4TsQ>Fw4gE{@qdX`qYccUyP^16hydE-o zX$-M@8_XRqfSUo>3_F&U%_@kY8-ee9fm1tZdShATvo_DSX*BZaf9(N9qJ{a=Ph~o4 zmR92jh*5Z_sJXOrUt8Qjz=oD#Bmf(u z%}*bG@O8}Hh=JL|pH_BBG^lGSDk(gsHpJQfY!_0$5vg6th6K5()-?0)ZzK-R--k>* z-3jbr3GkO@;eWprvi`GzCT1wA%8HsGqr$|DKxYD7ezHZN=_A0(o(Ac(P!dTlpI+P@ zq}sA!(w+|?7u*b$^Dy6!8p58)P-*KdAnx}50*_f#ex5nF<1@~O{_8P|>v}p`8#f1P zAb$>tQW*snYW{+?!F=#OV0sby_d~?={XbTwqp+1}UHJP}((vDVz!NfoiGZ70BGaUT z3*6L6Ep;UMQWPV;;GQkr{xDCAf?Wxz(%e6E;p?41r(8^5_x#Ij9Gvk|PRk*RhN= zyGV1kvJH9(n3ITIgNCb*mjNkY?+_T{vnc|K?nPfPY%)D%%iXIo<)p}#^L#-v(Bo!l zywYQb+Ux0S>aXq@gFK_A!xctSgpKyzY~A&2FPX1n{W zohAzoh#LJ@q`hOF4m9c>>=-q$f9C;*;fUS*MtJG~UZj7LBdCjA0+N)CqDdL=jh|B+N*r0j(sVassRac?<_x=2o|}f!VPO4nKFua zvaL_nC#Uw_QH>rIz~X>dS8u;-m!+yi^~%w#l0npQCZ_uqf<6Q`!fO#NmmwObnQ~o` z$=De?!dmt!-#iuA_&PaEdxc;(c=)Lu7 zp?%$}HN5l9iavX*9NpQC=hh|bq|YN9P&g7%=UqHHKBMqe=4rp@)Be(Q4e3((^4E-^ zo}cDs4bJMB4+~GQ68b!{-vkvtYITtACRJ{wRU93~72!pz3GJ+(*pR^Qgsh}D059Ic zSXIxxB;m*W<9?o$>ys79`~hxHTkUzsuZv??!rmLyWPDwE$uxs1d5?PzV_y@inBkG~ z+u^0nlMnn8##sIE7a3w@v_jfR=1s`N(NRfRHNjG;Tq4HI+Q9(m5Ik=$eOG?xx__R0 zP3nnU|C8S4DI)up$?Z}>Sl1T++jSPX>%cXm{X#rZ_I~a);1!Ojk~}rB&LWcXWk-g< zdBBE4kk4h#ezxHVhryWTJu-aXZnjDyNp1PDOhAcFpd_kf32gi$OczjaIkUkna+KFJ zPX!%$z@oFYwa4@|{<|GPNzfu5qg(e88Dh}g?}Q8K#8j1kAW8}QowGJsp|xv+!*t*n zhh9IewEMVFkSo!ZB(B|PD}yAzCf46|$K1DUsqQvq7N)I?o8p=V4a%xydA;%tcyQ!M zS(d?YG6a5^0EHDrP>am%!TAVD@J5h&)s{RO)HJaImbK~)<3C3|vqt8oY`4A)k9D=2 zi0&esF099u;ysWE7XuniF9Njb5YuCC%nlu$~f<@_eETe{9jZ)6dU0+=&3&blnI;a_blUk z*_3e07Nf$;(~K6FT^o?c&F0EJYL~R8vo+EH{2-x~mW7?}AiLlWP%Q;^bCdN08?i>o?(_xFAxuqP$_Gfwmsw{Tdo1mS!B!(oN^Q$~XlC6G_A$yzpfD4=-zJF!rN~f} zwKZ_o*bfoubK06er0xr62UKsTe#EamZyoQQ1=I1pnM;tUsjtiXyK;H}KqpZjbk zmfUyLve`*~m~hF(q^(nn?PjLg1W*5+iN~fuZ&(q1ElzjcVpx>Q+qJSj87b|Hi5P1J zmgtL#j|k67>=ePb)<~nIyFJZF_NoA`{JMM@Xp^UnqsRFq%gXL=iFN}?`!zwO(U8_#z{Mbn0?4_d*OS~aY|?o%&?CKebR*kQ*%zNB!P z(rEt4_81Sr;%DKJjQzYj^kIdo#AhfZ@R!}l{gJ|eK2LuM2T%j?Saj@-GgxY;C|p8e z36fDCER$ zp|r9e4EYiL7j5~J96d{4%_EYp$67#dB+X|rY{(=#96_~V1ubBatYLBAKCU<&FetHu zjC4W<=35b4A>Zn*%)OTID=h6B!d^mHuTo9)B*3(!{ZLu&enEabY5?j*Z!gR zn-qpP=W5%I$Ey?OhWU0zav>~gDBg+phCXXb=#<CFZoWqbLW1>Zy_~aW3=F7Xuc!u-=FxM`-v{c zRmw%&;g?4;K?BIP?37QOzwNDcHCgu6A=6gn+ zB^4$Yl(VPIA1_+XTOpDN^L%_yCLT-&gwd7;@NHn1>RcMb=5Z!S6t-)5bsTx4e- zzN@;eik`gV{6P!dKy~9$mKqYh@!KQq8eP%rMELq^%t!a-lk{EcbKTFm9)!C;NwrTC znNvPTl=ePqv_AJ5wu?z=rAZtN#l^n{yo!6r=UB|oV$7PcB4N0iu84kPVBNh`Bx2oc z6?wex_10eRK+$8nQlL81oS_<@Iy=jy;$icnBgNyX2eB#Nf@{B&-}zo5lTZ-6s&L`p z@=flz46KR4Aw-~4cDb^*` z5z{v{xZ|_thw|MJt8B5g8Nl*&miu+j@WwK-7)pa zIu;Uwr0pN?N@t-`3`f)*sAwBmRlcQEUyaO zl#-+K-Rb&R^>gWxW{L#$&(+~<8gA`R*dn#nf{$5{xz346ZlL8h?IC}4DPP@y_By}l zyeL*9J5u#0D=zWXSfO=6s(*m`;rY$oT=atrvAKE2@jI!r1ZAB4>R<1!TdiEKpXdM? zgZrt9*6lvG%`dmcH_|MX{ZJckcl&1r zDD)@2-44B0Q_J#pjJge2V{7Mov!geSlqE6L+3B=4gGt(~v>W{R)Qs|r%Aeh!9Hs2RK1#M4CBmKH`Ia8^lPzpzD8@e6!=;b#=~c6 zm*--mm7STvwNUt|OIJbHLJVCC@VQG=wbVi#ID)DuZj4M<9~jr{2kW2uFEQ0$!>v~5 zW)^tX=5&r)-X^YdZuh;l)SU_Yd3iW<2m4WryOhPtCvBA1LMy*J4cjambz>P}F&iWv z>Gg2%=|#u1TY-gB6IF|A&N_`35NLq6dCc7Z?6|>u7XH|7FmXTQD>JTuIKWx9A|k0i zD?FQFzzch+R|^~Q8(qgA5Q~Nd_#PP?xh{F{8%Ml134wvBT>Sp#j4q%)dYnKog=f5` zNg&qLdf*<*ARPHhC$qb$1Q0&7(}f}coJ_DxRBI#~3)~6P8 zSrH6wF(660-RCM{ovRd4+jPs)CqkexQ^cNTb)o_ECtYvwCkoj4lsc;DQ2U`dJ#8{jufKp{*gfRe(?pHO^)?BZg=#as z)>QH;^P1=_BU%X(`iaq!(V}85!E8*gwzROu`0LDM%;N4=&(a^@l=F2qXyQBNc2sp3 z%VTDqbbr#IK-9ZgV7cxmuZ*S=dnx35k=sT0j!f(<0b(2%v7lg>P!K-lim0|)ogU9m z?LxF{hC@Mqn5W6aiJ`NV0n<7)TQ}9`ilIGsRDr1h^$AGYQ$B`?93H z;z(#Ttx)vOloZ+&z6jZTQin)ZSG9k8B?&Kl531&1aeeU%EO^UlED$+%M{RYdbJVzI&O?|_Lo8I zB4XkkLJ2gV!bggvLq7zb$6s)ye-Bq9X?tz^GHXw9wPP<-HPqw&{Y! zdexqx*Ea5qV)qJno)M|;yU?Mkta~oK2YeiMtH%JaMPZKQXA|6lhlIm;^n$_sR{cl! zvpHM6Y5!gA{Y%UD_)ZVI0FHz-EeC7^*~$iNNaBwmG&nP2jfNqXz~xVh9YG>7U>!!c z`o51+FMF*7IpuUk9frSc-PxJ{_|Ae#0tQ_ZNXMm2`z$iQxN$Eu0nUz8)$vMeKzc1a zU-1iBH$LW|7HJ*fAZLsXF4hNnTNn(FR<7PH$J0B{vlDfLl)tR4v|8DZa%j!E^0q|; z8WwVn|IiLWK2Wb`11m{@&l5E(pMON4Bm{V@#IArE9Odoq=i zNKrq%zr7ka6DT6T&UqUM)b4mw;tK?`t7e} z5Lf^RMurd-{)w#9xQLFrr@+>$+jXAW(zU{3QI$a7+KM3t4uvhU7Wc6FEa1cN<3vJK zUl&6F|Fu}J0m5W*?)GcI@3C*?ssn7Tg`*|bHp)W{DKHE{!$}3O>L6$A-aQ18+A*rb zx$A`P18?ELmmu0>V1@jY!INwxg9M>vg@l9{|JfEoK%<4V&pmjA(yM((XMWM zp4fYx#`82F_oIHXUImD^`!5VYUh)?{3V$$?>N3rHY8?Z1B%2*b1)2>!dW(1FGhFNJupJ&d3lT>DzN96@jC z8ZutvWIp|>j2_559J5*VYo#F|01g7P66rnw3!uLMn>_@%3jK}AcZW8I5v24qN;u+l zpuh6AXzymV*mlJ$>Ch2pQXqEJj`ih%RZFSo|0)j9qO9=qiP$i}bMm3GIq&ZW5EcU;;A)ae zf6P#H6^xU>kX{jz8bYx=(IP-?hogy;F8^~R6^A;+3ejO5OhQt*cHg(BmTgwdibQjTqTR7jz*Q<9)9HGa2sA*)bhoi~j9)0zFR36l?mK zvd`+Vz8jMOKVIr))i_&^!@SeNY(dZxS^xD3eT8ZPjoGR2Gm$R=j_6grs}S+klBm!@ zz>+EHB0EyS$OULv_}6<$5Y9TAWb{00Mi2%jafq)?an1lja!`b-M$()Ea7?i5-}5ly zw9RktjWvGhaUcK18ET`<74@>KSQTd`Fu9eytr`olD*2TEg&H}-WBz}luC(=jLK)Z{ z8Hq;rtJ9uXwm8){s||mZg~*pp|Ik^^pfWc02$j>L`(*4iAIqGf8@x||eV)CO9m2Cb z`|o%buH`XqfXFnf&eF})vhIXKhm8E!AzI|%GHFw`I{j5?g5gYv zi}=sFsm@^FR0N^n!JKA9Bqk*yt1#4jiknG0zY4`)sB4HV;NEi@-y;Qx{I5#daO@cZ zwnkOKGyAJqz${070%mKVI>zt}04q$DpyDX3mnQemc>!qk!y{sVJITn zo&8304mLoKJ)^j)>;GCZQ@wVE^k;xh!ceG{eTI_#OOBNUS755c)j`V1a%oQR*`(r*?yoR09041%eaXrAj@LVanC}F`r|*e+2^p?h&=lB zrqZCEzHFs?URN^iEr~YK$v!hGCPwV0rt`xq_-CMl^fz68Kw3 O=QNNyDy2$yL;nM`z0<$| literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/aerogramme_components2.drawio.pdf b/doc/talks/2022-06-23-stack/assets/aerogramme_components2.drawio.pdf new file mode 100644 index 00000000..aac2f141 --- /dev/null +++ b/doc/talks/2022-06-23-stack/assets/aerogramme_components2.drawio.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14dbf4a45545889babb2fdc6580399811366f6cc786cb28e3467cbcedbfe9482 +size 31688 diff --git a/doc/talks/2022-06-23-stack/assets/aerogramme_components2.png b/doc/talks/2022-06-23-stack/assets/aerogramme_components2.png new file mode 100644 index 0000000000000000000000000000000000000000..f9e2df14bde97eb67b8d9c2b6572deedc57da8b1 GIT binary patch literal 27405 zcmZs?2RzkZ{0D6Bab;y?MfSQj>0Vr}&2{ZPukG3+dqhSkBO$X$LZp!FkQqftW=2#- zHt9LH-|zST|6kAZyy}Jfz29-p`F_slyvOIoBxR7xH#DZvMYOUr^EQnF(5ax#|xobP~f@%i_L zGGY?o1_I`il7jzy7wqC6;O^`5&#ffo#H7V!|M|it&;j$$QGeG!=TI*-C%6$@7cL5S z)zSzE`RAO#Fs#cz=Qz6ryF0nyjzSTjEC03`Jj(swR%+TAn_xmh44@v;XfrfMT25N? zZ*QOoNlPzBCz$|QgqA9wl!m&wzAO@}t*WLDKB+nz=tyWo4UD}_!kuMh_4M?-F@By1 zC*R;;w7IjC8A9E{&D&7j)WAYc!NSbjQv+P$V`*-N@kBzwpAL@dGFkxv5-unc0~u*Y zf9+6teRQayYM88Ua40s+(ZSeP5AJGescjMvfYws?)dJ(CD&eg!q2}%#9v0?i=@8_B z@YV7U^bXOrkVKg1YghzoT51^k>jyh)`07A?%py58!rWijrKQA5d&>#m>Ckfdg z8B?Txu&aWBtZTT2wi?t-598&8^@qEGd+GZa%9(5E%AiBM!*oogLN(Rg%;jXgr4(F@ zyw!pPsYtyMpN3z-_+k*-$~sUJX6o#*#ZfEOKAjvpWa6LQc#~DS#{qKA8!MB?LY}b z*ATd_s*{0~Pq=}UfgU)=R}vYDd%I%~Kwwi7_$9fUhNV zrE%{RV}LTihO7D-`RJouHOzJ3@~V>FLFx*oU^ZyT1ezj1%xbt`P%a3#1zb;8&Ol!R z?yC(|kOtSHvEfi>*D!fgIq<VR!-qGFA&)HQyP(vDn_C&}AU{wuJCejwV zaP=UJgqx)t65}Q*AuZ)5Z30Cj&9O*hN%tUA2|s6lE!W@>A17%^R}G}8x3sQ>v4pXj z1!%OYw6;Sy!pjnak&r;R>zL}A$-y19^^lrKUAb^HI#4H2K{5<#YGf+w;%EuRRs$R8 zCllyx=x?T@?jqrfv^0{ynmS-*d{xnCe@iXh&`@J9bKNj$*$|TeJuM>(d3^`0x2KPq zyOaY6NY&8raCL7r6Y!T>h=rb^R0t}_MFNTOa13^IaQAkxGzrr;_t1mN!NVk>vX1_~ z#vnE|3?xw+A#PYdnXq6vKNL9NW$10F>*^{84fa*XNGm`C5a3{_zE6-$pmT_hoS`;c z2B{_^s~`inP={-4dpSz!%BY2EE2v_io><2KZ5f~7&_F|P^B}B@oTh~tRNX^WLC4Hj zU(HDu7fj&+^5A~n-be|fV3d2Xw7IW`x{+CU5J(rQQV1tx06f6d-9$kHEhnkz<18z0 zhMVeAuAv?#vW9YDSh%b^)YT){C&a_aT;3GzETiTsFD>IAC})gR*YrfVYB~9N`pLTZ z1cgCW;U?}{xQT0^Z3Z_$O9e@o2I_;4s)nW-dLf~>&Kqf|NryUuP}J~qMmtJdSXzM2 z;_{R(27$Fe;9|;1QrZcMYn++1oP(!JkcOiguejt7dF$48D<6kO&Mg!^lWm zAk4wFj^L2FygAZXLDLZBi*!}@!VN1z%Sp??Gg!wArKJW=kahHucQx>olT)V+N4{CzA8q_zBIbRG3&TmxO8p$J_C3v*exwi6;uS5{rh*j!r!>TDcnB#m{% zB@Se8K#-Z1iL76ku1tWp3lgoPEvstms1WR9A*JnV8W`p;<7$WrhNFW+;CdJb2S+fR z8V>I6j%xaaK~o1!IB1z`fUCN*ZU`>%=d0248GmLL=7-+AZUp}@Ast~?t}MA_nf`vow{MXV#KmxN zk%3>Z0p8Av0T^7_jk}J5;NOEWT##9^`ZHamk(+RAnk~3fAupE?$NJ@ORowDhFr(QW zB7DE%u(*fH!r&gv6Vn{T0GlvGV5jiPq?cp~v}Z72l*@eEPi{>-(!p%G{hVug;rNzf_PJBi=b{4I zhu3fBrWrSV8!y)!^ybsLn-KN;z^ogO%Wkw#!Tn>E(cB%S)v+6YD&+l^-@m(eP0-+> z@=s~*v449S$`U)2CvJT7^UJr5@p1-U4XW_%H%A*Q88;qNu}HanJNY$sqw;}_=ib@d z4|@ln?2He;&j`8>-=AanI+82OruRVlHU+cTclTdkkyGhxauav?bleU$C$AjQYf}rj z6o=ho`0_d=6mfS+ZFzR&S{eiX@MFCKsetI!95c7>1afqfhY1NyTIh1`?fvCc zanv)*Cb#{K@$+BK@9yq?@hDOXdA!|zcIOXCdCoC{k zY1Vr4NGvfiaeO!*O1_=y{^9ZNe24dE*+T*A7XQR|jPo`Ep>m+TcaAPvgSIB}?4q>( zT%6SnZ4=zg=DEjn{`0eMhf5vhd=OW_q14jvkF_>RR|uW^97?HKrAttcv=wp=KpQIW za41cH5xPH@EpGR4K{K>&IRuPqIW?QCVKw^6^u(>_YAS87b{e6W(f5gz%n{IcOxthW z5A&g@gxkkkbscVdU*CB>$3Lz=g`IS+ImF_Z*O@=(q34BAWtM^HhQ<=GNa_T7Bl;wS zP=Sk`_#WeB#9eKyHVb?@xmT^_k9%luNEi_=#cp|>AMc9LBga8#x_z5?^ulF`$#fC4 zuo$#ZL(XR|IapaD3&iTZ%};d(*TeViw>~wv42h^5@H_GEo%m4L2G2|kc?(jdyWeChRWn-8MUmdbT) z-t%?HcCe7J*g#+Z$?3tC0t0ccc=W}&Zp!K5kD&xorzCv5ZJ0fgBl$L&Sk&9sW^Gr; zT*v+yzIVqU07K(GRA}{lXKnw`m(JGMRAJW*H84AvDJlgYC%Tn;US``g23+QOjy_o8;XNY7EE77yfnuiT_~ z>ANu6eQ~^!ajqYJyu-cyD|CJFdIG@_<6(3HcJYYpEuK*<@w*ocvC7S-V#&HGJ6&m^ zCfU&!CrMJ6QQZ%f1{h(ctj!!@i!^{v7 zA;COfXk|&~bj&eVs8!*+l#Eb_s(q}K&z!ts)VX{|D(q5hs@8q`h!{CihpYVfAE9co z3n{2Wi>;|=Gs{q~X$gJ}`*o}&_boE`Afpi4-i(2t=D?6Nf!vfn^ICnN#F<7q>+{!C zKh!l-aD8umbX`yt!cv3wo_*n9e6?W=@`cl80qY&x+clC{M|Gd$hZdb~eI^Us-SA5D z6pTXhWz}qN9`$9FU|9u zQ>`d;xHrEB%k5Yg>ic_dNc_7-&uhpw?`3?*QK~r~ggfD8rw~cQ)e3uQ z%lDTd1Giw(%_+lGkrk@4o>OUG47o_Ti0qrWI3*N#OQ?H^nJyor;F)E-2oU^XdT|Wb z!}bzJVvh-Py`uHA%G<|HKfm-=5cmOtqd~@dkmx1N4Jm@1dUJ^@5eRt?dL}5*w+H+U#5R-Y;JivYx-bE_qsIC=$GP^==URxE`Ce6=W7B9GAi4NhD98sk{~=-=S%o^F?Adt@WNX zTIxH=T$!xSbhXlHWlaUA=s4=Hl%e5J0V^V0cHXJGV^_{&s41u|NCUP00(%Vn;sE_;=n1_r+ zJC~R!GbvD0)x2ACmcJZ=+|i#}oQo|Y`4K94bdX=KhcXlQ3CqM|6@E+k#wz$;+_f~Z zyLvWk2fg|`oEqO`_2>cK1Qy0!QZ3&6M{+N*7+>Z+9-ZlAG)`h1XD z`)!E^%)}1;MG;P&OG{Fj(zw}n0(1Vxg9pJPz&MHE@uStznw^ZRX1*pKB*FyMr)8Gn z=6-vV&1gTaK z@D~o0u0FMB32!#v36Xsj5*igcFs(6%gSES| z5i^D=Q@_0tPpeW=s+NRmaDFF3?8jA$(7~_bN=s=kVR@%wjAvX;#6&QP9B1=wg98Rq zSs>>t!wy|{%^FLru2wep! zx)D9Eky)~}HXFh}39z;RXw+zR*!~&JI`s3#34*% zdKx|tR)e*%ke_q!ddaSCg8uSlHXUqzg1U>?v`WKAoBdWA4yvtQ`RI-U)Q+8jmXLz8EgIX9w8`T=Dt1EKoi^>+JEuItC!*59>^(4KTo>9x_mwFr7r z^Jy9mMfIcY1q`K}%433ORk(LV`68b&tca&XHO1bqGnMhD6hqtMb1lclK=l+Fc6pen z<%{|(POk_+=(wBT*FW)DX#e7hXkcb25SNvKP-PGNghCR{>>_gp^TZ8;Ha{*Y)@vjt zB^ej1M0!8oDN5%7TYQARn*^c zOkZ*8H;ScEt=#3tlaobKA{D1Dsi?s4U=LskQm(@ToC5h$E{>i;r^N~Z{I8$yD*aN! z+wypKmk*$NZuBEx?6m3jxi%#IJi9E`sr-%PHSlDDb57gvi@R6O6~NQag|) z(Q;17WvO^*1r_NW=4bG$x9)Nf2vv|(5Qw{dNX+F{Be|I9;s2J*VAC3S+wL4|go7=% z(Z7SDvUo45Y+eVq&BP+r#X?JOL>y;osRjZ#n~&q!!^2IUpF-&^+d?J(cw43{K8z%HlYPB9e4i(i^)L14a0bjXE7`hiHra@jh{*Jz+(N`XQT5B8-`qT^0*J9~ zTuncXjcxVj_M85Z?k~5Z`qSC^$F>(bitVm&q3g?*gz1T=u0n2@Z^16?gP@e{?Ke)o z(qB}Qg!MKGgznmVg}y6Ye9G>FSbSFA++dM%rc1hf=kxm|l>!L|f+fPfK+Tm^AA>S@ z=h@Nr*cN9i73HN()r{x0CBDbUF^puVv7xXUyA4?pXy}zKnydJ^Uo-#fZ@AC5f7GnnG%@?_HD zX0`03Z@Qn6=;T>QaMm|3@ev3!h&TuwOC@fNTt#r*@3j21T2o!{$m2Sud1#x1wFpZ?I}LJLMn7ho!t_cPSMWHm*7L)UcIo0St3%i! zftlAD?QOckiG~CMSvkcHiJADCH@>GfHj#Y_LI37e=qd0VKM(CXRqN8^?qk}o7_fh5 z;8IFK&=nUWB&gbVx#m(bKCFh)k*qL_XD*v&2vT!(dSE9mX|;Dw{&`qedO{55EbirQ zPV!~e>?$PP@;w-9y#von{JpMOr@K78izPQAN*};Ka8ZD!3VMHjsYfYDa66VNS&$%0 zEk!h{M5<#7xi_bo!Z3V!kwFyk)8 zH&IENDW=@XtEW+@NXL+E+^5~(t?9UV^fMjO6K}>5csZA~1_5C+mw|iPU+>%2MG?t8 zKiMM0XqMEnW# ztch&P5B<9b^>w5v`T5KWgrWl3Fy?%XLXilqMOPz27S&6VI6@E48PDP|=Z9Is$n3v# z-bx)IGcEe~#Q3vB!dnvW&In(-ouK48qvkyJEFOqZbDw?fttKM%fvbq*j;^sE8J9Zw zDdiYt{hiA|SSS>jpVb)?nG`DqrF^lfr&*$%Sa|-tXCx+OGKEpt-;0RgqrO$|?j%r-&S302yN4-f^S&Itz@7?O_jRgaiz; zB-`^4Z<#PMfe+JvheNk{(X?lm6(|NY{dC-7G&iZH7^Xx9ZbkaNWa2p32IF*a*#kF9 zs$oau6VIRC<)Pwq->Fx;FDag-MQ5#nrCXwvmH$=9;}t$|S9S4Gq~yvaNQBq6B|%o) z+qY?c$)l+Ke$MC}KYT7eE)*b7n&c&86pTXZ8}$BNml7M3t@#ki9C0Tc;>7n)o_9T1 zXNV>1dKP-HX);Qo*10kL+-XKAQIF$b=The{E^X|yQh6Q()`p88U!1OadZ&qIi0!nG zl4|$z<#IrW-wUPWf4Rjx(|emf|NW-GsBNv{X{PKxZ;6zzd8K(sJyNy5#FuO3_(n3_ zKAkC7Zx1{g9=@pI2>=n1R7?h44RtSvYR7Xi`zHyjmreOUCZ&?+XPMJtC6{;!!qaF4 z&uPZszU6~+1cLrA!02R9S^7RhG}8j6jU{}@p!l8u~=EO{#ER_-eo+DC^rpaF<@kpZnX6XY2XWdx7=;bb?6)Dz;LO>jXhQ*J8>ec#>EPA^Q$J2S>K_Q z24%@>S8zjWyS0a|=jJ-SbRQ}NSkd3jMNf~eqXLE|Y zs>{mI9FVp3kvKJmtn-){;ZvR&Ow@g@Zxc9{cRqhl)slvR2z`KPvMovbT8T#CR^LqMIz9;++vG2)sj z9997c6^>l`aCl}+olZPiu2J-z;(U(Wpv2vwVJt$DZ6e74#~6z#_=+r;0L2^9Pgm=5 zo+k*HKj&-ud&3S_6-zOyuzkLg$3y%z!&dgA1sxTChws-XDy1A&tj;C+)@);R!IcQRwMy`p1&$@w zKJs87s{;KtfH4u>-)P$*xE^_mCZ__7Cr(RWl;9>}CxzVh*Y8Un;9A?s!dQHzEI zR%y4+8%Y*>K$ziDz6oSvD;qEDN?&?85&5O}IM(C0()S2RGDbjfY`fLA=}L}5^V(tR z-f>f(F~>GtSE{&AK?FEA$pI=o159kBnZUR6$x8|(aD2~2(PF(eKrZH(S zS5yZm_a*2js4!xi$q^JX%WUzLZn?pj=$)fG>gM3mVjjnu97fLWkdUS30hb=HM)ELYTOqv@0FN-&NkhwXhW-CG^{ zjHDCMs%Nli3(dkQ(ZA1*GVU@gB~`rwzKjs6BH7H|%p{5%VH>+3mA=W30OLP|2PugI z!{0jk?Ba?shi{C2rNP_a{h!8@%rAvlhBw{OU;A@3A{70<4RaGb9JO*r;XrW;=M9yh z@6%a}iUlf>if_*MhR%Zsk7hu(-}ykzliN=>g(+xhE5V|3Lo)?dJ5^($Hc}vmB87lkib0#sya=EotS@?)-!lXzU}x+{@Y=(~ z9uF`lSKqNiW{JD@L`QD$BbL{8Myx?%XdN}#I&lpgJ8}#~+p+M9O!|MW`L)nK5g<11xFR8S@l&@&%8OA=qY-*xrrq#VAl z(=$a7Z1WGv|Gca#V0h9Rbgs17xorcIL{P2enB9PdjY{;=vVMmmlJ1o8C|_yA>Nwxf z&KdWToE`Y4xE}nC4OVs9Z~SP#DVs67lBaii?DmLJ9kWQI>~?59AG4TYzg6qI|7rm! zuP(;fOc@r-;YNnyXAW0|fW*34(C^LKb@3PI{w47ff+uzyI1EGJ-pA-deSmn3Yheln zW65OTJsy)+`Z3)uEYD5+Ol{Vy56+2WCRcwki~Zbx8qZKl{b~;L%I8a&wEM(Pk$E%Q zQ2SzQ5)0II$ApG#G@VhHDH7Og=e~`@MM`-%B}pmxd;RYkppYjNui$lh%`~*c-&W>j zQgNZI%q&~96>HTp!j?0JmK*cYgGVL)&!f^ASc+a3Mv|Zswpg*3IZ<^^y~$lp$Nw)~ zNlFlCZ5_v~LWFC_4=lgErQf%o`VIjz@ z##Aybx zU)&YXJe|}?nR}-K6-6vWor$gvWc9464rOu^a$FD3o)6y=bwTZaeMhhIeH(}u*;lnL zd3>tn;M2XIS$8{gS&={}RYXcqRebHo0U)%MfZp%Dn{4i649SnJx!CEU%@#5ddH}`H zL;{!o;|q6V?Q8)$QSD1P27`K+msE=v z!gDuW3{p@PNH`Gb9)WgDw*twaf;>0gW3t)^=%rP91+wQii9C5X!T~M!3d&q&1utlL z{Ko+hE4i9SAuGhfSK_|;z1z|ef72TmQ@=b*3$gEIaF_=L83sxrKff+SpRDE;<|GTl zVKJFu$pm``fQ(>=Swv!Q&F_va#U(y^0@i5N6zc+yCqMA;zf>SQUosI0ofT&Cc#h=1 zdNi()LIwXas1hH9m&?;8u@HG=bLaZZ>k#qR=D_^2&HPknx1PtHOeaDCthhc+wzY3%l)s#4W zw%>AG+=z>wdFJa+zXzzML`c$4eN!V9+><|}5{G8v%uh-NF=mFFsDxr#cKJ*jaUc&? z0tLT8n79|S)SW2pJu5wG+9(cONGCZekxR)+S!H)(23dw}s7Iw|r_UYS|hn zNY6FQyhfVa2K4$5Ko=d!y4Hmn|mje%ps?f@D%woU9jSJ`hLOum#2e*%#)nKV9?}O zP)CvhiFPvl^lHE}K9DD8-OjD7*aW3+E%_hjM<2YM8`9IdzczVUjFfLYuib9LuCyy7dfmr9Gh3tk#!OkH>H zt)N3U=`()bCJEwH_sn}%o_;3#zQFg5`&rm4(=YDpl}at5y_S6Xckd6PMf4(*jv{?( z+gtXjO~`LX(WK^yYS6FiQw_L(dE+hTZQhc_KUQ3U04u1a{R=PNYG+lDu}D1r7v%V0 zid%T=l~>q^%8b)4exC*8GT+hdB>&K5IlRlud#N&j_bGKhRp9q&$?nXcyJa^+w#O$# zbvB;W_|BZ-anHUA;H17R)2Weiak1qqbgF;ZWw9+F-1wGI$k{m7j-|>d42JbCA#47DB_cxxS2|L8cBHZ0k6`NDf zm8Y4Q4H+>eQl$Wj)SLewC~`0mDI2tj)_<*G`EoSw#gE9dL%(}}&VSM3e5SEhc$d~E zdFF4+Z&UX-pVn=DpPFL&X`^Op^3rQ2`?IEy;>3{E3pctY7VAj5Yx?Xe#qTcghN@UK z1{E7#WIjtw_45mapYDIlaakl!4?Mpv)tkAT@}a_rNz_sufXa|3Bd}_(JDhnG>%i_A z)99WCqArbm5gX!~;NSyZ;YZQXH|bx`Px_0|wf13yg)-2bI@CPiAzlkQ0sJGShu3Gv z2k^_?ww4UM-_L@|tt7%$nhNvhPUDNu<9T#rc~QuAp zHs?{ZUHhtruh8A$=iGVFOIoGTJ}TzmX?o|o$`61?}Zaf@Gd;X*Jxmw@E>6fjlun{KR%z4V28QN=t z2!ks(O5@jQ-_P8`xMSfCcfixt}%HmYt_9eS7)F} z%kF~uIJVK~m&5-t8hwL8m~n0De36?Q;iIH2T$p~z-L*a3O6*_r$>Z&#-9#P(7D||^UJPAPG3rV_q~e69C+AP>gl`wZvf!m8k@B^Y*t)~Us92=fgE#lDu;f1azU_(;xi0t@0rb=7 zuUahAwp5Ldq^6~x5Y&L0$?cZ4B+kvHkmJj}x6c5B!lp7f*!72bG*_{kv2?ZG*&%F` zt552@n?ZQn2s_1EtLWb^&sBjeo&vbRt;hR_C7HH%b08mj?H+k^B-U;Q=XD)9f-CdYjO_ zM|2kmMQa~Q{5+)l?C!K;a9N-XB7;m@| z=iuese3P0@tX;$?uoY0FJC}25n$s;q+z~E&IWBvIjt-V7kkBu$Z8){>%X1&m3Zw3t z-FttBeympW_FyE_7iEP;hMcG9+(Fg^iq*ZuA!(0Jx6{MESJ|>QmaY2-e3pmbP;7#h zuRW>x^m4OBWqx5qN-Tz4_W~lR^Q@MZw~bS>1*^Q{gU@RsV?T?&gwL(%7Y*gsKh3LS zaXS6tCP#R$Z1bRcJoVP+n@huq($PPZKbL!NzIw!Z;AwbE*VpjK^t#)Ji}Z7)OrBmt z8J{o7gyVG0G@l;4M#u^Jo6&@gDv?ch;N3W0BvcIF7q|{AP9?QVG&@ z>UU_H+in>?OShQ-gY(-I#+PSxz**NkJW$nIa*X#R&ucI1^e2?ds?03|M100Ef2V=8 zJV^&3L92?T`;_a_cVK{n%o`%qG~~0hjWS zs~vH)r%qfI&@g7!D=iH3_W-7dlj<5Pg$VS3>d5MuhRlhJk!-ZWgBGt_fS#Tkd_Mg# z{meDjYT-+n*}e8byG1_#yb3AK?8o&J4hgWmFU8vqaZRg>uEAMdE^eGk#KIpI{o)1S z-9#T7^~7iEL$1qFqFy+5Q9ST&W;hFBu{S@pwSRi=H3FISdVshuzdZ&Hm71>)zD%KZ}Cu3 z!T_NnGVAjUhD!}A8p~H+!rd0(!2Mjj`6@#$Z`(AGE(bdZKHQ26(@ue~5vs;I-n}zW zL8eXpH0PU2{A&vTGA>h6e=fswV7hq;N>QFqa4C6C)!YuYu8)J0h+JXFwp6<0&w@WoBg^WB`H3dm6y3GR3Tt z-LKu;&1t)!%(j>3r8m#BVsC($HB zYWBFUfo%#av;m=%klffeQxW2&K_N&|m3qG%I%WJF0j^%2>Ns9~pZPYe=?|Kq@UH7(@#-hr$Rs)b;+str!|`f}I00CPv{z}S3S)IA&QfQs5M zqz}R&*d6d8{d>!Y{xPg!J3HhFNxgSCKCeCrG*ih_qKGAZ zk;N0zFvjzG4APlq&O+4WGWM_th+mpOCvidH-^I(D1f^idE*bZEnkQf>_LBHR8Fy|w zT`_3@T2Vr(7A1oK43vC1^29}Tu5o0QE%hV?aAHkjGdtzdrg8B_2Nzo35Y(C!!7nHzVDw8yQYjLjdLh{&2z^43uB#rr0Z}F0xwd3^b^=(M?=~vzH0iX7ZSOa1pi(fz2|byEreq!_r+x1Zq-|VSe}~!pMO8ThFFAO1U7J| zj>n5kAURR1Q2x1Sj{#y_?O$azMV8oy15CX+%={nk!Ub%!Laz-z^W!07 zcR!Hd8pye{nW`If4g|9C>dvRa6$7mSw-T7|k>LbWrv%t7`ywOJ>MiRg+pkgZ3CJn_ zQ^w_FNYd?L$57g+({I=GYvAlcqYyP!QjrVb%9$`On2y<;SpO6ty7C^LGB>IM-A!yF z``R}y!^veJp~mAwLW$?TdDy-Lxi9?c78xabJ5u@0TFoqrgf6oK?+jDN-4XDp?*5a3A z`wV_WRz9wmvY5XwyZwq~^VeS|VEz*#`mh9e6`MIX`n{Ukvm_iJzh7Cjg>aust{9fz zHDknl0b+g8ru;dd9oNCrbcU9DkxWQf;)9yf_5qwn@>etB`7^u?Y6`v8$++=c(;#g;Ew4@1p$QwdT?)n|?UsNX7UAv$I>idKD-Q z^`M=ZME#>F{WYq9RH4_iuLb=ZKuTxB*UGc?tu?s*6{1>dEb$M3BF>&1&tS9KYqZ#jbIC5S-v91z|OAZx2&#G$doj- zEL1GsejEqQ$G+#1%Kc8M|Gxj*X|oz7vDu{a@cpc@n?2X9sup!roFQp+W;v6Q+(pNGvT{WmT<~r%+Y}}P!QnVLtoK| zg=;X0)keXrsqdO$vBES@-f}l^UEV!{f89a72L+|raty3PE&%rX`IseT0&usf)N zta7`6*-Z1{bhF$1lu9)h$aa0v5AnMu?ad<6+P|hFBz7EDt30T z-S0;02V)qP7OkvVobBu_aCP2RRfPxJ!>GXXV?W?4Fj*zHhHQ^k%B+Ye;soR#N(ZC3 z$!~ypTZ$t7Mt1Zwl1aZ~op#tJm|(cNmj`|bi~rJQnJTfH3`m=Ap9LK6IJB?OiHWgW zQhu@km6u**-PHtJl#Uq?!fa`G)aPxUeugD~V`u**M~_~7Kx#_4F~5~~5zT+-B-NVV zvmIrc)Y%q^S|-CAG6cI>;}Py)wrBHd(mbm`5@XU#{wl9|53K%I&YOk&|2vCckyjz? zg}q+3Qt6;?^L&LP@trQ#pW;(1^)8`-So)#mjOt#3EAJF zzN6V0pK1Pe9aT(qIYZWUP(+jvsh5ju)y9=H8G)e8Pd6KqOqIu=??>{kCHM#H>brYM z*m`bciUn>*BYTKvzK@V*E)V8+K_n=E!r3LYHm~;*D_y(N4+hMw+Craa@`!({+l*R$ z^fSKNf&=Gv#%c3Cq~`xmsxIC8Osxf?b8Si?6 zdpr`-q#+#aST{SMm(}BO2VDTYvv8EtMAryVV=^Cc&EHQm%K(zY`Ez7H@`;^dO(>^? z_JLcBm|`yi?UOHv1eM{F;TM+F|C!MMVs5|ZR=cGPYT|bSqz0T@prRMi{2$l#a6x-s z_jEoR_7_C?-VtP&HT_!-;PCX{asZIKK%FP@!%yp}e?8)_)_DXie+ByM|6hR~loD02 zm}UQ`&kG8nV@yB%btT&qC|MklJ^gz7*hcOVP9Fs@|2_T_?U`-=x3LMbK+mT+Ws3T5 zCnqBv8t0rV?)cd60i@A1pZlm?WX_pw86X_2b)$}poC13|8qS$ zR^~RNZUq_sSUpKB8Q43Vd>e96p`D{mZyEb3?eAJ8PV~QtWdLlFq)Yg5$7?T*ldiLn zMdVY)rQ=tgxaBV^di~w!H(UG&WL^J^YP-;YIrKJU6cT4erB3$bPnUIXB-3!?)|cns zxDc5VmPh#zo^AaSIV;-L7JAfIy%CDSmqEbqezQl!T}bYHsT366c4;JF`;4j>ObUZO z{bFvfUfIcui2Vo(2pDV*Ex2O28z>0)391W*--~(+Sj2rKu+HScZhx|pu38qUEC`%r3m)16k3@Ap3yFvv}`*b?~VsP2*gW6{eeMlauwrQCR^jcmF&aakM6!Fezk&T9*- zO@rbs3eR{QbiTB;{cVz>cmpg%*Kd;lOS0g#XegY z)m&lGsG$Dl_VULwpsH(vWCnXfkbJh>lLGB5t*L5<72)catg>3*4v-5RE<4QWV7?kF zht#P6Gu~%`ho0I$0C&N0s>*KKKpj+UcYex@igJ#iXee~%_?}anTGxHzj&Rpo(x}kN zyyw)*Bus7yp$B^E`y)L1XZ%uaQ%qIP(j^spt`&Ai46rY-6>?UYC8`1IDXg!5K@O1v zl?!^P>e>R3BO&3p_*hAuA?_8vP6yf!jJ>CtX}K>YQ2L1|HeAU8StxLsN(z zWSuISe%7;+2k7&0MirKtD+3v$1!iF8W1HoaM&{Jb13V2?JsQWxWEpt`Q9P-16_IN> z$cv2rl|b@j-!09f6b?PeA7SUQTV?=efRFH)Z_1c+jbpf*%Ov4_=c_I!99Tna6rRZ* zi&=Psi%TK!krFn4J(eSw!4Mp`?Eq!>p$G&f^|t zA8o^6$e^NJdOl`!6pxuEYq(v;DU$krQ&5U<2Z}RN&Pn(i{T3g@au8_9i(x#*<$bNA zY={Ab?)LZ3m^hj%GsYK!@r*1YcYGQ;Jt=+yyOc|>Oj{JL(08jB;en*|9&CsYNHV>f zQw|^O6{p{mV7FG8n&Ayyqq!-m_pF~~mz>~G>HQ?#o(|mgJ3^{X)j4IpHRN2)q zJ;E+y{U$s)MM;)0H-dUM(<)tlXN->9{DhEuRkWP~95|u&0?kv;cThM&Lb;Q8B(Yzd z@OLi&9zdDU1VYdD?r+4PEuB~q(RabRb^fe_gb@iE2DmqXThQ@?7y*!t-#{+ccqWHJNiS~bz`+gS91&5Xf=M(21JUhsPZ5Tz0zyIM zfr5P=_>{=LX8}&xAcqAEhe0|_vkyV2XThJa9o)|Dx=kCbrr$8EfB7boN~jA8r7b=JNZS0Pr!0khdp6qMIOm6!JU` zYoA`^-$Kx^A+4Q2&oc8=kC6U#JLGa(X`2qAXPbt`O!2&czByg9DyQ-M!|0w0Ow`m3sW;%9!zWM-KQ; zslD+eRoNyvHWN|x2UawF!Qvpe_6@j}Yc20QT#I=Rtifj57zbfLLojH3fcHDDks2lf zrQl1;S4JpQ!9W=ePMriR7p5AXf!3;lj^G0A0I*G8Wk$tVM-t!^l|A-TV4gg^_m}0# zZU-HG%0B(5eg;=}9USs`n^*&{z+QI+IQmV>O#d{;vLB=15|*#?E(O zGL&psq40H?iqo=?-tscv5&`{iBS*x&MQKPWuxc~sprG26XY+fQ3MbEhXum(EX2}8k z%QT#thR+POCRU;ndU-Aue9&P`{k@p=uJbsGp~mrtVG;$0&!28pE+v7kK+|G@xxC*i zSs?`_Z_4$yup3{WOx{;T#Jek%jb|E8G!_;S!2VFnf5sHA`~6Whwv?DJ0D3l6D#z)o zJA%`D9qwAVm#&KrB6fWS*kRqqw**KTgrhB0Pl(FQYLK8e(+E93pi3;S_p!|nhZ7CC zxBYpvw??`)QbGLuHPJf^t3hVtfPM4Kl}`G68%9IKtMrcR72wG%>MXdz9ecz2A6kKPU+V@eyzR=5}d=}AhXBznoG{rvt4oG@ouHb-Q zybL2Q1aqegEU(K=kS|#!wX#Z20KR6~cx|eH&h6XB%t3-w5oaMcv(GOgQq6tcRY}c7 z_tBmI*rzJUcpwbM9aB8(v!gIdy(L)q6IDvHM6i5(p!}xSuu5E$b>pGwMI`kTFR5l$ zkGPxl=ZFp*2ar?yMicdpV=cOP@%|c%_OW()A_0BW<#fVE8a%1eu|0+`m545~+|9K5 z(UE{&$AuHz{ne8)5x+HyR2LLQ!)ofJvpm0KT{e+nuL>Kywr8vWD3k9JmeofhUo$!# z{2Z@&)O+1duO_79iP@j8da0jnXgmA`@oJ0blbuD25=ZD^kfZV&RWw>a1KI9JE2&0B#9?R{;^Be$@jqCBMS%;Sn=C6E%x{N>b`=B6 zSY|`z^7oQ+^e452oFoGFspB$aB10!Dy*Zrc-!XrtJ7ftCJU8d!%wnGEX~$9G)%~fo zzjL9u=G?wxPqaRvzfCeko4#P3 zs&is2Gp~D@>gPB>%SOSJ)YU9g5S~IjTs|rVY6|=DG~L9f{7OQmbuZ^>54b_f$pJT1o|{h72Vp=aB9*r1M_7IW2Ry+M2HOn$1#F3P&! z46ZpXl9NJZKa$WtXyJw84DG}oG(|EPIXj}75BEdfA2qP>!M|Fn|1wO-OQSYb`V5b~ zRB6F1BEqXhWXT3sPW&l9DkdhUQ#HfrUjAz{?LBUn%+R#HUd@E( z>oSXm1bcxjxIm}>aHT33=6OC-wXWE4JDs-*5u z6U`#qmeNS1)36bHkm2p?^lOAZiNlzz!YPJBPvr5238o&}1POxcoN9TGa%(3cuOsIa zuC1Q|k9IC-#`Vun4vImwtXs#iC0zf5v^%P%aqt;6?$TMz&@h`7r(TUmmkg3p7_a%! zXo0%AZ`#BQPc2I9eEFL!j93$me^zo^7x9KmR3G>vkWC-|W>8v4zlw}y>{I%^XuU`% z+v{vw1WaV2 zx-|OsGS#$B%kc#uM05`U@h5=fHIHE~utV`aAShXz*j>w3$o4W!@An0@o-g3T@NzSd zFiY-Bcms3@BuTmTNzsX=oIV&2NJ>&&$6P5&Jd(rim^Mpa^dt5z!-?oZvpR79!cq#U zH!r=j<~!=S;av1zVyKq3m)|@n0Bwpc@DwEhTcYLP;2&7GK_|6M2*JJ3|ycf+XX5Z*&D*fcQuX`VVtmd!PGirz`bk5trNDSh(z zEIITJ%B$PPbwr27BV zfLQEs+7trW7|N+&QO6-RMhWk(;9n~Xh@*Il=z(B78_1CY8Vs>4e-*z&s9gqIlq<`UQ7NhlOsGzztD=As3f;?17u!dueo z8SXj{fi>2%=cpao_ZWl{yU17;Az{Ta1v&%wBpOqOaa8nkZ@wtLe^hsNDLr!}Q_Zw# z=5muwLrBZ%GE7;Uxudzw)cScfZH{l?1dLgWClBG~NPzDh4K=xf$7^O2W3^Y3lPj=j05P#GCoz0>if#S-xO& z(;he8*H0<*@*s(NoM2+jta_MoeO3>qLs2OS`w+Asy3G{~I`gO>V|VqWs5^q26*zK^G9nhGuEK))X!aoePx>lg5) z#PNEj!ur82lJjj64=DG4O6h+w=fNewmK0O>AZc_+%Vdg87t&Po5Yg5S3H)LzF1|25 zZUcrXmxqB(HKO{-8s(yG2l5x5EY}D0|EPcqKKYpRw!ZHl{_~?Pr5dz2?vmYsd0)j{q34ui7$@*hvP{+ZakcK zo>9YE^9=qufjNuv_1M_6eX7T#-QP}gzsgidT+#knu>BuLq?N6^ogkS~5N&p&>xK78 z>+u{_L=syYCxAZ%y~Y%==z)Lo8a`JAp`%jqKT zhixBbUZ5vSE@qQvPqt_c`HkWPJUd@(^~&5w-*ZzyCgpgW>0T}N!aM5EcUtMj}3mSa&Qk9M%C+#*s2zXzQIc3g9~wK zex8;&HQNLx$~yPsK2IObA0Nj))Njd)#eE#B6m$SqU}e%9Zm0s|V&69tLBWXU3amRh z$cK2Za@qpPefku^8YR&aZ5L0G#QR9R0dQ#cT(Gst#w^P26DTQ*RCGE0WMWOx_?PZ> zQ(Kd~i+5&r&Z8>AUGLYe+Ir$TGaO5tR+An4a^Nf#?)QRKkwLJJ`^!vN8SD4ZNoQJ` zQhWF<(W-4J#~jN|XWrbEra3?UIn($(*Cfn(WrIB&FEK*TMm+|UK|eacZk<@MqB z*GyTkGSc$5TINta_1=$IpB(C2EUteaW#Y!b30GxH0~-Anw%QRbW5fx{<;^Nm3m9CI!9}^Jg;0gDy?rU2e6ikR%zIS&a+1wJ-TVWPW>ab1|Jk) z3?rwCT+((t$#u1ppoQmgPH#Gm_u82S-G#(4L*A{Bv*WQ~E5C9-Ox-$GD!eIKD{Q7t zd2+TtM5}gT<{e_M)EplvB~ZL@!fX9;|1OW-e#cjV-|uo*t805E|M91sdJTYp+Tqrm zdJLT`qpZ8HfFYW?5VdG@1W-ytS1ukyv_TGrMg!PK$IjO+%&lpe5O2Xma=Oun8`$Xj z5BDfJl!rjv@)n3Toe5jmf!q$|mVj!)O%Z^^O%?Zo4%P=ZdM)C8-XBq)6$`lpK^q+q z`y||a&1af0QmF0WLe4GDt#!8X%$w=q(?gCoU>Li%z4iTbuL{#!+_(9N`0%eHC*4`H zviueeD9gajy8s8pVpwVt&miJ&KUEWo_W~P8`$0tzGpY67v}QWJJo5W)d59o~6E3*f zA6aap?|kMH?(jG>F_CgYf<<0JR8LIJfwON3y?h^ck;`7WMuI3~Sf>3w^5)o!dA(=Z zE00O1M`t9ebfPSo9WTpqTydxU9t#|-emj!TgVawg4JS(Y2Ttb}-;-d2OOZ}8MHBr@ zLXp0iAgJ4NN$-hy3$?9im9%BmAFF>Tp4E!oOY<{1)yPA6Z=!EGu-$VgWA%27E9{Fe zeqjB!aOp$VQwzaTWn{?TWjY?fCmKf^#t;s#juxgcEtK9wrD8H2ZIPtKVA8IkChGc4 z4&v!EGj#WX z;4lmj0Ws-{kVBx7XrOeUi64r%D3wT_@dNnI7~m{APi%x1|2B!ayrFXM$5tcA@1~q8 z&VfxKhMk1oc}~4L!|*?5OMw3=^q7ZJa0}PPv7IKT#cbbtJ15d*gNRc-vyY7{Slp*K z6P5klo@0k0U$0+nA6VB$ey}M1-dL^Pcdjxnp5O5d!P~0tPKH84XhZ8Eq*>yz=!s&W z?HzYDjy@P6k4!7KTEG@*Zy2HxRAaOMJabS`;FhN28UaaZ-x!Wi+0T(L&|;Q=?_O^g z);e@k0dt368^VElj~3TRwc57Dpy{UyTufDd4E63yKARwt14ejNc%pTyBxWBK>6N5(E&{U$IErgpXD{kmaH}rdI}-SM^liGD8-C2b z2B=EWAS6k{57XRF$qu>*GnBD0t4AOrlL-g&wI`jP`$K9;LLFgam+GrZ!slBvV7 z>&0fBx|ubsCs|l(yy!B>`MrC9*_g9xTPL}pOf}r$63p_Ys=^FF0_-`9Oc27;6f@PH zGm0lhkdz{d9sngk5@Z)%1FCJ7e8Bm^3qe?`mbMT;Y(7)o!3oM6QqQUb#IR-@UYC|l;yBG=uaT`V6tWU?#Of<5Ew@N z;%gsU9m%KGx)zl2J^(UC3uhKzJ~=6Gk}T2?kY*yR*s&zA+3CgHEg<6dgPHyB?@@7S zSg9wNdan%hKf^2MHc2F74UuV(@z$W^(_|B?RA{YT?GBC%`7sjVi~nAYTRSrkq~0o- zc7IAs9got(PIkLkQ;NdI*dqyd5e@oe$^41bDS~hehZ+oktJg^c)rLEO^#?gV*|;VS z>}}BHPnl%uK*^BkGIfUBz&4ua3-;~DcR%>s757yU1+=-+T;|d0RzfLg zS{qmqCV1A3@)p{FlggBk3v3y6%D@IUHsKyc#w>n=VGxaiqC_n!(?Gbr{MCj08AXxmRZU(`B3}%?4VbDM|9>5L{ z0B^8zqKtb4EO9XWwq;l{En&wDv&TJ2MFL6@$fxWG{8=MUc5rUR*mS*gi>L!}tMmd7 z+J;3;&W#Y=oPuKurGTcq4@4oz}`rub(>>rI4;DP*TNpN&OB#6 zaU9YAd`FQ8nKVroA0%Ui1EG|RmGPQ4t;Eo@?9s z2Qhnk)!_zCQy@DWOZGV#J_XQ05b-k7AOv{xRAW8}p-wB0ng7SM&{x`AjeRqo;`Twq zORf|X@Xii!0NLUIq$Q2Y=_~ftk;A%$V+OgJm0~l95EE1Yp-1|wOm$1%2?BC;=aWel zTHl6`M)2?wtgVI-53roX6UzHp4bK^rD))d4Q0*tF4XH1|v<8d1<-A~eXULto%) z&VNFvhbUuyj%?eFojDM@KZZRLe+twR1?2u}@s%ca@=MXI3Y3OG6N3yqrBS@9r67D( zw3HTCx(awFEE6~boG`m59qgVfd4yPweh7-$Jx^$cvQ6duF-@jwb}cT~xC-{1q>ct*1~1tRV8<&u~$5p^TTKyBsQ~>9=l(=hSQ08Mu5qGBT5{-Vu}Z%P`N|8 z;#Y&$e{?N$+HzBGnj&7J4zr`lFEy>G^8X)A0+Qw`3s-kSV&-5#0z$hdv*SdQ7{u5q zSk=&CqcI1#_E&$}2-vVl)KB@nUmB`2GH^gY*)P@^NwK4(2!6jY=Ez>n1~+<~IN3Ls z4jwIV{cWO^U~$+xdHdWXwJT~|2bW>{E)#}e`ia|#{RRdn3YY|UtbPs(UJ+7^{1OG; zR;7=8PMyj^;Tz5mQ)(CnW8DCQF9RO-2ru|wTz+{B`g5HyHiUW)$&Dqml-) z0r;wb(@FK25(4nGEri%;0T-+hVL)&m+(|41Ke%e}UYm9%Ed&xqAuk6cHK?b*+uQK! z$?VsWG!&=}VO&%m@;{(lh%7eP%V$1(9GK@Y{0n-E3jrrA*3DuJPCFdK&3%XSf8geN zd%r7+y87gol;Cj=9wJJfhol1|>SF|e8sr#x^&cXFXmb^;o1}&i2U0dQDt8XHDF=5& zZgQgkJjvk>J9w>CBu$9+Ut0tIcrpTzu@9qUfDS+kD-dFNtsmRh{rf=;;BgH9eozD9 zwZk|h!9Y{1=@TzG%w=gz@rpv+MbOvM9j z2y~ua$d=$UpqO7+)BZgYzKM;6kBS3a9;b|tfY&d84I2pqpFxr&aa|(9!DE+{A7#08 mpZdmjmeuSGK;kH0;uH6hth3Xd*@MSiT~mc?DU~Z&h5r}6fw3q6 literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/aerogramme_datatype.drawio.pdf b/doc/talks/2022-06-23-stack/assets/aerogramme_datatype.drawio.pdf new file mode 100644 index 00000000..44146ddd --- /dev/null +++ b/doc/talks/2022-06-23-stack/assets/aerogramme_datatype.drawio.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21a3bb004ad35c8b47f14195c720048f8db31f47bd6346c1d747000570149c67 +size 31073 diff --git a/doc/talks/2022-06-23-stack/assets/aerogramme_datatype.png b/doc/talks/2022-06-23-stack/assets/aerogramme_datatype.png new file mode 100644 index 0000000000000000000000000000000000000000..c3b015a1e9b8c4800739efaed7c49feaf8dbcf80 GIT binary patch literal 9090 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

alVseQ?YF;t>H1wKs8A(HUHbewn5bfzplr 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%0DDBw3u?Uoj@}IxP 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 literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/compatibility.png b/doc/talks/2022-06-23-stack/assets/compatibility.png new file mode 100644 index 0000000000000000000000000000000000000000..ce364a9b1d34712c895ef07e153e57a8a4e1a66d GIT binary patch literal 84505 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≀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 z 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 literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/consistent_hashing_1.svg b/doc/talks/2022-06-23-stack/assets/consistent_hashing_1.svg new file mode 100644 index 00000000..f8d24fd8 --- /dev/null +++ b/doc/talks/2022-06-23-stack/assets/consistent_hashing_1.svg @@ -0,0 +1,301 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + 1 + + + + 2 + + + + 3 + + + + 4 + + + + 5 + + + + 6 + + + + diff --git a/doc/talks/2022-06-23-stack/assets/consistent_hashing_2.svg b/doc/talks/2022-06-23-stack/assets/consistent_hashing_2.svg new file mode 100644 index 00000000..5ac8faf6 --- /dev/null +++ b/doc/talks/2022-06-23-stack/assets/consistent_hashing_2.svg @@ -0,0 +1,334 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + 1 + + + + 2 + + + + 3 + + + + 4 + + + + 5 + + + + 6 + + + + + + + + + + + + diff --git a/doc/talks/2022-06-23-stack/assets/consistent_hashing_3.svg b/doc/talks/2022-06-23-stack/assets/consistent_hashing_3.svg new file mode 100644 index 00000000..fdfd3efc --- /dev/null +++ b/doc/talks/2022-06-23-stack/assets/consistent_hashing_3.svg @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + 1 + + + + 2 + + + + 3 + + + + 4 + + + + 5 + + + + 6 + + + + + + + + + + + + + + diff --git a/doc/talks/2022-06-23-stack/assets/consistent_hashing_4.svg b/doc/talks/2022-06-23-stack/assets/consistent_hashing_4.svg new file mode 100644 index 00000000..95ed0e02 --- /dev/null +++ b/doc/talks/2022-06-23-stack/assets/consistent_hashing_4.svg @@ -0,0 +1,377 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + 1 + + + + 2 + + + + 3 + + + + 4 + + + + 5 + + + + 6 + + + + + + + + + + + + + + + + + + + diff --git a/doc/talks/2022-06-23-stack/assets/deuxfleurs.svg b/doc/talks/2022-06-23-stack/assets/deuxfleurs.svg new file mode 100644 index 00000000..c298c22b --- /dev/null +++ b/doc/talks/2022-06-23-stack/assets/deuxfleurs.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + D + F + diff --git a/doc/talks/2022-06-23-stack/assets/endpoint-latency-dc.png b/doc/talks/2022-06-23-stack/assets/endpoint-latency-dc.png new file mode 100644 index 0000000000000000000000000000000000000000..7c7411cdad839a90118b6ad454f05ae7bba7afac GIT binary patch literal 131776 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Ð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%0D(rJ8yrvA=O}veMu-dq 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

_bihU_DdT6CuK(Vn+RT{=@LE7^|(9{M{wd z4wK~M#lEt>xkuj$7+@{jU+kXe?iqv!QSupsM+Z(VNOlhd`8;>MRn$nYTD7YF^7l(b zywqi#3t1euZr#e^bHCHwd6bg8&Tx`+q4?xXQB}?P{#Uyp*tp|?0|z*pt4!w4KTxn0 zg50$QDommL!-b$Ab9Dz!!MGXglMk6~3QF2ohznM$>k7Gk%!`-x^Gw^Mc?iP*r;q2D z=tHQpr%nwF;V2h^pSw)qDas=r9enrho#9{9(rhR{@2T3Ev$Ocf)>PqIRGu=$D-WPy zi8}y`ldRGTo!?lH!p6#>Y$LR^{Fg6FMlNq#NP081f|SROT`)6erfv9Wm^i~vx2cd^ z7iT@s;*5{aqcu;>1Cvw=IMlRHul~fTU}Ca7d*pAwwbV7P;nS|A_tZN+{ynW3tn<^- zSb2uB^HQ16st^krIxnl6mZNdFc91L2{)cMRqNW7g7maxRp#GL=h%sXnh(1nF$NaP2 z%uEdf3-s5lohLY&jN)zNH8kwO6;;abH6_S>%cs5X7&V54nl#~-250Jpq~&tr8WJ`h z3D2edjDBi2Pc2hN#~(m3;8G&oKVjWnM#(rKSWhA7dTi|OMQdV0XipOv6vTGCD0z-9 z1g##KkUghdU6;sTjk$Jhyy?c?RXGF_7KrxFMa0pMwX6~Ct}1G3MYq&`>5w_?l}DMg zB#e*i;lnRsKnlKb^(iR}rIW!-D^B+&Hg0{Bv^GJc8dTzHX!YG=hakK9jr`mDZn1sI zN;>TK&Zf(JZm$@RGUjLZ>D_w=fJoSXIBHM}ID#@9j(+|6RXL&K8DHPXkQH)MM8Ok&ySt}ZeYCw|2%1MW6|86D(RcwPHme*U!ay%#@C6< z?9kz>^|XD>2?flEr^?N(;U&uf#Y4mw5Ah`O;@huXj zWLT-&JpmI1&o)$%d-&K)xw&-Hwr#VhiNW`N@zSM-4<5|4uTFXT^s4HB!2<_Q)zFwi z{sJNfe#k%Gd+YYN2-9)Plg>?5h{@6Gy?e!*)b|vw9UH6leysrPxJF{R#(zhMXtzs`rF*-%{~*SJlowTc;dt3M_#H_`?Wf+ z5+s1~z`QCG3bJ!Dm`<`VGMB)(MI?NG{t42`a2QZy4`fv+ba(f3wy)yS?7pZ>9y z5ScpVP!fGab+&p38Wnx{68X)8CD~K|+)v8Fc(U0OU;0DDI8G&zvdC?&=p4o(p$Ikv z{;u5YSt$vywniaIU~w@z%^c}u9t&*smMvRij(cI(qz!6FYkAA-J#!o!@8hd;EjV`U z*wSUoq71HHtku4I?_S~csHko8OraWW{_L`)b+vTJJPtPOF?6aTqrbkZ~Vq zv$ZB#VIniMbhg@0cn<7_h@fq|8_TV>wid&29uk^VpFZc3dbc00#xq6knH_Z0{`ali z(w;qgmQEs^TTfG0H)z}sS6)D+nAd(pmXvbv6<9>^4h&jPbmU&S-o-gb(fJl`6)`St zx~BBp?c2ADB$k$L&ln4=FhzWonej^EtKg)feM2OKLsC)_t5HE-Sw`mW!-qwJ@8?#t z*I^mx;t&rnPZmJJD-JDnb#;wptx7pG5h}1Su87JGKqx*6&qm_?EaWDcI~ z*s_?#;lr0w8b3Z~pdBO&owP&OZQi=ISw>gHgrEmG1AKhyaa-b1d4EfS(`Fk%;l5HO-(<&* zIH$(zk}_&)Y9mMf!Q*Ekl}-XS^Al%gI>j5UJsJGGqik1~^<1r}aMOEq^w6QrsPhXK zE+pHL>gqC7^n6Bl^PO&|tD!n&N;4%E+I7WJDR=I?6CN@c!JvCAZQ;U1IB@HBLh}$A zWgEoR+}q{#%a?YrQT7XQSx@h^)~n2G!deXiAEDlCzEDm`O%&mBZdw;=v7s4=X}obz zo5iw&o+16kNnxw3ZTImE`Q|+Nd zo~!8RSw_>gj-5Jb51MXYy}2f;=a?~Lh^}HQySZiILS&PXo67jbBQ(Lyu`R;Grnj+4 zI*UZl5{gm3)z&`iqk>Qc5hnP9Y!g&3iV%(*95i_F1cT^}BrI?YoM#el{{GF_PlcJD zoE1sUOEj^OaXv9_m*yd&5A7LUclYYvoul;o&RMJPbH1?+w;>6!M90?z>xB~JHisWS z?c+n?-8#XD9DNKf@^IN&8+ns{DzHBv=eTHQp&YVlaml@S`BJJ&64V{Z;HVLzhF3;r zJ@-(Ixd8p%BC#bLg+%Ekkvo0;y4~+%yUp2Yxm-W^IXufu3on9B0mlNQW1g3ermm7p z@bv2Of^t@CDl}MiIpGF7ao=SO-45e`S$+HN-Ou|#;`s5oJE2H3hL+D`2+a#N9(D=K zoZvv&Z~_nqCUM-}1j1{7l}^7mHnH8wND!ZbcJcRcKY3UW-2Y;A-GDJ~iA&VnL2a^Y zSrN&B7uNF^0~+FFA?7 z?+3R81_pA*3pRj$SFB_@_ra;t?@r8Iu4RJqg&Os7f5SeuRaG_O)*}7@*J6NYyLK^F zi`1CJ-Sv-T#`j_{zoCMwBNl&I?J zZbT{V)Uo4*T`N~U<0wl#(~2xxW0cOL;`PpV_FE5Cya&^;g44RmMt7g1Uv zR{p+B+t1IhP~ge5pD8V$p|i*{)`))k zUNqQ>S-W;^OlN8VeW&iwoJpR3sdf);+L|Grljk~aCH%B`0T#4{PyY9;yTZ0On%Dil z)$&cuQiq>f(dGW1%FIk6B)+QQ-BniGd$cl(t(W?puWe#BDCQ8)vFH8pMU-+ zP$VLu5Hez1+^_)yF3Bp(%L{6w%EUa?7WU^vJ_oI-7L4`Jw@%=#301&|lX(y~Py57e z$)94kLmN5+OWn+!VBhU=oUJ;?RNEp7zX;ouP#6y?wR3RLE{Co$0-;<;!M~GIPTDYJ z;Pl34O#L~Lk8@jL!Reo$)nT@&-fL?=$#W!ly0C7){pUaC|I`EOjt`qQW`x%!*>qf^ zKRB0k^Jy$eN>ccwy+b(f5Nt%oI%KBfjp_PdpkKxrUrBrXxCtx1zJjOCW-F^7phSxi zDx*eypmC8`FHy;qo!;J$TzrAh{=;%^V}_z0YFJ1Fh*+u@jB-Z?PlLp1pgz{__tc%h9T$_Rzz7>P^YDQP$eNU3AAHU%hY-LBI5BSEz$Hf@_JzB(`Cp7HX}A#bQj3&8Y1`G(iDqt8p7CFvKyfx4n{FRTIA zf;ul%0;yildbM>Q-626@dC~pFFSL5Bo#vH^@H8Y%J2*>Ziz2Dj&>R0Q` zmj|{ZBR9gKzbYuWmQ2j*%sb}pDdoNHTiU*U{hA8P5I|#V?$P$WIw(kSJUL5(;KWQW zyh|jW&9@u|4!pJ{zgC1J_-4&Oxk53r;5E02U1)nsy8V~ga3RH;CN1^}2`S+Wg;Bzo zYJ=p0ED1K+y?ZyBhVC)Wh_9l)WB81CfJ^-2|7l}bAu63j7w6?Ub1yLD)2B}?9NS%+ zPLBQeLlP7eLXGMYIP5M~^h2NWohcihdM&sjW?bn4O6eUUcaPNReWc<_8Dk^-t~c?D!;L6h__RY2AjUrKHfXGiNtq z`rF*~XDix4k@)0Q1O6B3b2W|GZ9<(E>=_l^5S!O?dh_-UC5nn=QD8d zgo@XT-WYh?Kit@B%DQ7&t#9r*|EDlpsQ>u(9~+NeYF$n_5H4)qKAHqzB%j$|mMvR$i&27-RHNL@&5hCANvA&=-uBnL z5g%{3dbM70qRo4tA@vTL5-e6CuG6Kq?4D24neS0ntu$rIF0iVmgqm6`xPIlA8M$FRA zuC6_DFDk$7^zS;*SP&OkRrU4j*)i>Z#fwu_@&JSv^u4dC zsH%R3EA**2m+$*a2YrcumyptN_)cOwvVtrV8%wWTzFZ8U0;?|Cc0{|zL3E&@%v4Fw z&Q^vF@BU%c+$T&VPM+P~f0GrtRh+k=*>S5PJ<5zV4~2(^3z`%3$NzJBc>rlVcllp~ z^>oB9I={5_Ts43G1HM?RGyooub&=}de~hdwVs7&5*GZ5u_d9`?Aa9)|F{Egw~g9jU~S@S&HV1@RDU7WKE=}DPKAW}3B<}DFw($g1nqVRIfSbd<^ zSD$_DCtGm=(32YC3e5Ulq!Inm<-@|t=(e~Fi2%`(gbkv8=gx0rvc+}_lI*@+%$Z06 zHc@UzS7-8u!bgKT=(s@UCa{p`c?r%#A7uX^aekkL0dxE=F|I3Pr);11!}{(b#M0X@33Y~{`)(^ zKOlJy1%_TF&%cjn3+VQw80iHaJ1h5-)DWdz4ju$7rTK9W$~m7vk|3eUJ=eJ7=6!NT zO-W5{l|&Yrv8#dK)EzRas?|)asJdjfaU=IZw2(nCs7cA*w?C-meEZ^$B_q~YpDYpb zrSQ|I>!85;1^xisW}0+>1wAC!L-#i~H{(PK8WG22JD!;Cv`Le)va_SBin@0QHk5m= zzi85=No+iiu?~-{ckV<0=Ov7Vl<0VE`HD8s#QAC+xYXU>fA8Dde}ZflpPCp*f?%3{ z^H++ZjS+odgxs2;3CI|?fw9pBOvxCv)D1}$L0UnEeiYN-@Z5x0G5my^+ZBrGxaj_I z6H%=p&Xtw3A?GGe%uG-J9Or*dv!xy2W`3dhZ=%Sr?-msNK=>yWtqLJ0aycL>N~EzyYrncCOm)QDZ_Vul z`PnrPv)+SAF7=`u7Qr%3g32JB?IJDYcM^VfiSDuS+l*E@zOW9xA)->Z!uU`#hz(tX z6hs;5P;&FC5Wf6))|@ZP{`({oK)i{xM>WAS2Rl1Hi#2 zo_D$?x^WrZ&N{^6kqHR^St6rGf%WTEe|%S{;%+t=2C&ZRFSmZ`xYa+AIpvrG+ar5x#{` zxOeCR-4?M19{=9A`z#j|JMrtAS4_iA*s=zmH0 z(d{{5o6)6w>$NVQ+IQg$fu{$Ln*WcEQnxPB;yL1fM-A#CP5X1mkIWD*tvvm(j+Q%pI?6C_Ia00a+cKm0mIppFEi` zdh}e09mSc_s2sg|9nYSAsAr2D=K;oMrIPT&hZiv;uVv`y(H>*eTI18%bufWcRtO9s z4#?q1*`i<*x|%|3b}e-IB}s8q!>B1)m$PpE{`+s5Q|>BEm8wb97G44Tv|aj+**tzE zv%(TFpKwc1w4_=uk&T0P7%BOlBe%3LKH7C%b5S2n-x&_O5bq%wK`Y+&-Me>(2OG20 zcj}(6e<(`Lbd`~jxiBA!LMu=2J|5MKjAi!HjVNqEN>2KjpWA~*zX-Fl?khs{bmwVF z1n$^-=I_7n3B{902x(oE(HJvdB2Y(;mM4LgGcF=Cun~+bn3VJ;W1962q=+ijXcDrU z3nP|hpPSdMzR~x`{oor=JE`AF*#WhvoBgA5yGTQpj+)H817l?0Z*_IvNmoPl6ztaO zlq)nGZ?g}FMv^|U)r4*mY2HOV>U{{Wqy!a-zmq^espc=l9 zZgAUq;(2`~h#?XZCoF6=hTfW<5u`^mRsYW)K42~DAGdOB4ZYEtsoi8?;v!D*+9Cx_ z!Rgenxu4rz1y>Xb@dEhXckfvz0on(&4Oy$*#3h=XK_u3RwhY-{#~JCEWqxHElaL=B~$0)L8NC6zvpswgQjXs%5AW~6yY zs4v`n-Fzy!1ot?E&d*r3MYM!_Q;ql)|aa z;UJ0v?~f8R?yX3DhP@;5xj*o@WhLujAG2s!^Kz*6ZKkF{l{tirUW|#A8Y7F_ij&4M zvU2%6bKkwKxnb}}@+r%Zi^R!h>Wl<~Topnr7FZDB)@j%2c%dK1sM7w6Aka`>9}?ht z=uk23da0i#xU&5xdkOXYK+j4asVY;9KX7l3A{iSFBh1i>)l^bj}|a zN#cR<(+zyeVNXk_#TH0gYd0+3VO@fGcCPlRkP!1q?Q1JqW<1Qzt-&9_t$)c!!pirS zlA?W#03on|U8DhjG)mHGC_Jq&fz{@D?{j=8Zpl&zY?flLpqk{M-F5^cX-{u9Y5#!( zMWd}EzfE)q44yIDzhWK1A-h-Dm10@%--1f)O;@0P+`c{A-0TKtpyJwiJqaGUE0~hd zu;4@aKd!FiCu>qG11#m!x;C_B2>U}fV!=C<8}wd$)R|3n7Y|T8n@O-H@D@d0|Hs~7Y@{}O3zgrBwwf+ z151wgu!}SVI_JCLMjts44hSV?Oa97+VBii!Jk|9Er`^W2UjLl9$keOXuVX$SNh)`d zghTAnt5*v#K47|?dq6-79OVS#gzs_lpEfrvgT?z$n^KJT!{$QFT_0Oy8uZD>)OX?xWJw2fCi&Caeu{F7bV z{P9uW5E9VsTmVML87g*EO@&BD6t`fCG!ui6`hg3^MG{Qm__`)spbAJr z?QeBQQD;u&@b9AX#_cMPN4AFpXYPBysv})Rg1vaL&Gzk?d&-G;!W`XInl`AjDEO2y zD~y`#F3R@k5lZ!ZoMbDc>CecTl z+gFpqf~NA1?{A&+_~h{sG6EMOYa)SV$i2JvY&go)mDEo!%o;9Bb~)f zpu$cy>Cy(wcaxb`9T2MwOM4!U0;5{k_YlZ~`k+uw>XcZoAutkyAErY9mu{JN11?bkfHRF9Vdlf)iClM z2Rfuv_V-8R+}z+rWjnp9j&7LO(w1(`_8e6)Y11EnjI799Zp41n(hwE%+*qm=&n~=$ zR&?;-0;;(vGEV&CPRDZd%?9(Mtv+}c#gTiG%v>95WF)V|V^_y_BKe;5)$s1es~O`x zW=7GIV#!t>i^oNyOLe4ao}CNh73TYY_Vn>FaCDd=?W`4)hpi(g68`gno%P_|?kA}N zxby7Ubq?E7KU(P^uX`q)K)DQkBN%sGVSGlVgMVLni#&TD`t#u3)2B?F2=f%6XGoAo zY()}KJDU6h=#@QA4@VdrNS1qRyCCQ>PcKv9sl>GSvEmqEv|EoJC=2ifh|68NcOPHn z7lX+W1BY(JlLrqjrd&-q;<#Umi^j!0mcl%kS ziMa}Mv`;0tU$VDdGB!u;*d$fe@sDNgGf4UEbfyHaCwaiPWnEuoDxFNtY`;N+N-8QU z=wbQASJ?JBU)Lj*?756P0Ml&!s_3g4w`wRbl4DKXVxOQ|y`e{;|5qlx}8QWF0 zwlk~y*76tm`6H(9$xKVrjxF)D`(ZuPoAR1!ZMr|KU}dMBuvL14tsH3C96{>!58{-^ITOtD3k$$<(lEPz>P`d$Iw$kNRj~+cP zE%vD*Z%s=Z9E?eD#H97fv|KlId?Q9G0n$^mvvt2LACZ6=o{U^|%9IHuJE;2oTu~9V zW11R@r?4LvJX?B`Cz$d2wXsB?ZU}S#9P9!%x@b-UzmT4%oOvXb#atuI=HD9|2h0j8 z>@@z{h&Bx;A-q*xyn+dUE&2Wz&^7xkXHUd6{ezNs@81u)PziUj;!{DY%3eGFFpf^$ z4J10k9bEE^`Q^jgpqnU<(p}t}8|nV0);Pw(^TDYTCnU{{&k}G+z_eAuV`5_9iF4=! zWw6Kuq`=>v%wvbhm2^Hg;3RpJ!unl)%?5^sT9(_xqP8nK2O}QmRFsw)esqU$6a78J z!AIqgqBzgO@-IFi}5i6l{?l~Qxdo>on6gZZ+_vu54m z429@I(I{aPYXxQJ<;7EY&H2uxKBW?Q2&$)SRyK4S8wCl|nS&4-9*^HJ<&B4~E!e?B zQ9bqtYCgTTLC^5X_zB5hNl<8I9Ls}3b{zctb~rJ$`kjZKt;*!d!`M^gF!ICraAv|S zo!R(c$Tl0YJq-;F8ZywZ5F}4BBLW2)YV`1UuU6tZCQGWci(l*e3xm@{gaR-tc^ZYz$anEu!q(`X~Z zpI(47Yd})Cx1ypdfVoG!&*BO~vYZkk+Iwct%0U9TAmu4qIq{cq;d1H#YHV|bDKgJ# z5Yn1*PzMC(S}#VmALu?v#+f-bw?zjypyPnJDU&DLGCGGRLJ~G)i0If8G&If^L}5Ip ztGR;O*_|SRaaQ**Ak*R|t(zU}OSqW~CN8N{gYG9t3sv^)*|*roIZBv0?0fLwLs1iA{{nFG(L1g% zV`ZSu;((b=pzaXZ8w4X}&5&S-i8!S1DZ8;VF_DzAm@kU<`h8`@1H)0IWP+<27Vbq8L|R{HEU1{ zevl^;@3X7?;a>!;8@;@MTMrggiXoNeG`Y9nHG(1>FC zRDA5A1>V6{v-{oM|ERwpZwk@-R8*vWkTM-HkPe^Wg;yE~J2!7OvQ41X=gFtq77o3Y zo*`=hVrBsrCTgqNqmK)gKjEkphUi@oj|vJ}U+6moZRgR8uTGv=B4cTE*i0%HUQE~< zU@aA6SIYGRpvW7$I&6xIOmAHYi`%R5J1B5$uApLZwd1s2NRa$Ix>3EUeNB(w7cXAS zqZUF-+*qWT&>;DKN7$Y)WJ%y{^bGeWRAan&@q!#dcauI0P_2=>P@vd&l{m>xtpOql zdXNqZ9wr-l!**g6F}Zi((eu#fiPT{X8#Kt^K)iQQP$7o`T@=O1;`$39NmMda>iQ1f z`CA7)#m_&7z3ZI?jJ8C?zRrV=h~_3=|@yUdk_7B>?JZkw$Ub zh!gO+BVdkweLqv2`0kykp~Et@IpKl_#c9vEKiKo&pU_+=oy=m@ym~cbk{Fa zm7{%xd^|R%_{EhEM^nQRjGq;C>@fTIzp{7ybBwBpkGxPBRp@iFXEtgKt zo%RL+a5ytUFaER>?Du}k{=+qP^27i@NNoX7RHo^er$moMvB(7o*%>0ra-Hh(>$wSuelu>D0Ut!2MMLlU>Y z)BJf?@jg=V-o|o^_x*UL@%a}|V>i*+1%E)vWo5S{b7Y5ZNu$Jjh?ml&RNL87 zbgdM+W!bbgI&JRCy-DSAm?C!6@ z9K{P*ZaUl^ckrj9J@q|Ql)sAEv81b+SN{2@JFG%$2Lj{&3I<%*|5l8TBjxrtY7?nC z_soMsXjp<-z4f&}^q9EmJ5f<6Ll>`L$AA7wc7D{b^xWbJ^N-$|QzLx-dxC!6fZ1__ z1|ON(RP8s{N3Nf6@9f`uW%vKj8wC8jp}D;OZs`agOY1%8(7~TxAA9G{bdtEG}Dn5egck~086>Gg=e#%E;g)R|8WlZ z9j#Bfpo~b)VF{>3L_lVT>U8`hE7f2$TYlbx1(+bb>Z^c+&w`iFIavSpKZoe>Yxix_ zpb6p7YoG@k|7G{Z8oAxFq2fuWbD_;G#{+)Z*mtAUjs;yF%l5vn_&nQarjOU@^71t|d!ZXl7?s|qA{_ek#1?^gp;zc(X_8WV5TcTnl6^7nwR z_20|V3wCBWy_u#pMd=B?empo&h{R{ff1)FDEz&-pl8rCkpdX?egx<=?V$sblJp72k zl+Pfr2gfg3f}zQq^o{XCe{4;_7_nW=&66FRsyn%#C9$20&^4wFS`gdu!MgRZjRF(_ z(BP14-|V)wW*ATNWTHP8jD${u zy}{U#te9~g2z-dQ%vbnIugL%W`s>_z^9(K;eG>)HxJ}4O`AMO}aK|V-D=RObfdTX3 zldDw7!xuQh*El&k@^(eM-n|nOp@mPQ)Yi6~n+wHAksaUYK|syM2THDa<7)~ofIJeI z5z2vU#E}@Jkn`|kD$to!46=1uz%FmxdDd6P>#UtkQG3m zSa?(#F+?Jh^Ci(vU_d6{8o2XIQRW2`ob`{BaPR_T!Xmvku`ld~SkTLj`-eaMd z6}U}&{ikZDDM#eyyLpp!s0F@#7;V)95)&s-mXjIxh<$_WX^P`}9wBUE_ywE6uFL}z zue7vrz*mOOfY8xDH4otzftj_ZYr}!D<({QiDa^x#SIzx7I$Uev!qs$802rfcoI}zX z;Lw0mN6d!O?4>Yj)CTS{Fr3>bBo*xs@h+5_4|)1{WMg$4ynsU%P{Z~9xDDA;+Grn;Wj4)W`WCsjnRF2M-)5ElYqRA;=gerMh3X z>cJO23Bt4BHx@|HkTH%+0LBiPI*uZBQl?eW@nF&={?p?~Oyix{9r$}ip&hIk89D)N z!Mu4?imiBaDyYs11(lpDnxIvmO=-u{4?BO~MO!+to`0mAF*q9|_?v%vhOL2#|m(V0k}7<^mszo&H$tJiI< ziesJgn(sA0QgUSeVAoG5Pb%+*>+3zq{^?HF57jfM-O_44Hr(>cGK+ zcw-V_8d49>^X@dcC_bK>&&O|BJi&DE&h`k=kA9>ijr;#IF6W_dsz?r1#;#iR9ODaw z=;h0oiHNw=9wsLX!7e(Vb;J6$R4E&ub?IWRg{Rf)Cr`G*1;V!@frwIBDqpu1+z9+J zGg)NhBrIoij;q%h=Ve5YBBN%UxRpB6nM~G@$h>6Wf_ce5gGcH?)JKIyx?m_6rre04 z!wk1RY1An*FHb{Gu9$`DT~U!2lXhEgwDjE7t4~=?XS@Q7hVcM|*hboUZxq@GI_lNg zuVe#rEkrp^zUZ8Q&x$(}mH$qpPBdT)((-hc_6m(Umh9vO#MUPV} z?C`j;lQ%k$b+Ld3yUtfH{q(68c97POOOiy#j+J4P12vz{jKL#V4wjEj1YlF$`r^T- z7Nw}-^72Hsm#Ky5$Ej_2M!l~2SO0uy5st92`B1gZR3>R_{8h?0&$gGx5El2Uyk#?W zb<>DH3Pb4DrOURU7+-E3W1OvUINUxA?)PwoYnfiFkoxfq&V{ifTv#M&o=aUMS!cOR z2KR#j<@(&qVU*%tOk$N{dMig9qD_(?5;& zd8lmmBRlNzkD`ktAeOYsAhF~(O7WmOsh{+!fp5r_comB^>GHlD8Bn{Hga&ygcvuAm<}{asj?%*>g_2GUs*SYgN}^% zQul<4FrI?z;cP`Rot~GPb4;EQx6^RXo*PyJSO1!wo2$Plt^4ODgYi4RBgCfNPpNHUoIL@J_Z`7!mS7bY27>^ zLE%4u!3wb{Ce`7w+1^U_U>t(SBVCt^>xzw4CrzqEvU|2#x}kj z1}}ilREoO-d$P)VNJzZ6wQnC^M9V&lO@+oW!0(Csi4zmC9sr~QrtY}=J!&prj##!6 zNkp>(@V8*DcnP_W`oV+ui=?Cw>ZiQi%s5F-x;1&;yO+A4b#ZuKF^&_8P$pHKytYvJm%dz{bCilDjzM(4~ijo&Ef_oaRo9}k;K{+ynybA;i* zg&J!~!bCXaPEyWyi|hruM0zfizGxd}Sk8eD6T;E+3^NIc2mi#}1q-@K1NOxB1R;2D z-f@>`$sw8<8c5j%^&=$e@p&Tf#)0jFH{X+nL z@+Xeo$Em6K5wWkZa5!so)E=kFtfa}j`O`5kCzQ)iHBIJm;c1u7x0YQTBJ3PP)RMNp z`%6*=d48Qy9RRO|^=-W<{m`L9;NabqHSKF@W+y3F`aI>pmF~3gp2y(@Km?C(w$4xb zJ6T7D&eZY^uU7jOzf47Yh8S&`vP6!f;K#ako^{Q&J>6r2U<3|sF z{|U}h_cc2P%yg=*(c7yuT2WDP)TlX;l9s(c72Gs067ZGu&vOgSi}Kws8o{E^F}oxxZ%h%M%HlhpK`>0~Ie9B@o41=dztPl767rAN=6{WT#E%ZVWz zWNOXlxUoDDFOFm?Ol@PUw3aUI*|n>C-+%D9QN+7G{dp$o8o(9mZZB1S*v+d<0S?sl z;#pO2zQL{#Mse!;Ka}N2;nKDe71*E@q;|YNCo;4ogysB~PQMKtSk7-%J(}fp^r-e> z|A@-*x_>LjJVF0_xmbHJp*ej#m3OtohJHhCfrb>ZJ#n~MPrgM43JI6u_f<3r5$#%9 z`zU|a#0KF1)yZcJk}UfsdInRD3Y|>lr{6OdN>0}r9>v7ZS29FHDeIWm%(0mF=HlO> z6Px=2mYaG0xJxL|MeiEQ{C6K8dFRc{UI)Af%#CQdL>7k8ueYX1TL6G^*9xu(Rj2;V zF!>oXUisd5LL$);M}rr0gw*IQW@dHV2cPTJn_vEJ)Y)SdqJdJa85ABbIZJ@d72C3v z$W?{II(I%uE+N0}Ox}wZR(aPJslB*-<%+@OKYj;l`F7vk{KKyA`r~X;#!@Y-8%9pZ zfAb9Mgk3J8mRrb@o;-aa?XaMoO&zfGQqcS7m*rq;hFa4Mwho_DKTUGoMyB3?*v6TM zXdZ%G9`aPQ{^g79`&@b!g5Tfmd3k2wy*K+>wrniwb7f%uqicB~Y&QVJ6Ao z270*mn|7CE-|>C&VKJwVPG+eZds@wqDUT&wB#PC*9^+Qr z^pKYix&XSw#TI0LIC>O~98oNzBX0J&m;;T^0F0DfP!)gfU^OG;5l2Pmb-oJN?xxN` z@R=AE#r5*J)PQ)qq{g12SchGWSMRmRD3=4Lu{D%E>8hI)qvWxy-}-Ia)>Zf08F_U5 zpl=1Fd1Bi2pu-{^hS@32X?Qcf>M*L52nG}M8b{M!(%7KiZm<9!0qad<6UH~9o3O?4 z1p&;zs|8KS8l3iEB2dug3qG$)pJ%JXA(0K9+I~O!z)uEZc8LY2&y@-X2ZywIV`^+C znvJ+YL(0-=dgn~2csKnZ+VH8~)Hd0$feu`w<2L#}GFe8QDWw@lW_Y-}Tchl$t61Se z;k)oo3~o92XO`i0tCG*3tp!j{6y!rQ0s|aLW*RiS?Fz?DKV=#E@+L{tvG$OMoxK zT@0^AoC6ej9@RMD;vz<95s9jz^80?HWq}_zzB!YUdFw36v`3H3DZjARJ@07#q%b{h z_s%Co<7OIpZM=zo7}PXPW?A=tPt^G8ARW?~DIPP*t8FLV0Vaf1oT$7<#l=Kg^j+_< zKq>W~L_8p8u8+tjU~xfV6`LWFxD%o$C^l4vyl^r1lamL&*rPOcs-Q1qBkn|}hX>*t z0_jA*Vqd_i^e_KVULJ96j;1CJ$;XZAEXq9i=fbTfoTAuK*HBB{{di&0VRX%g{tq12 zXVkLm$W*-7C~65G52AmT&+60wHT5;xBTN-_T<352!+X$&C`(4`*vmaS5y@$fX}I98 z0m^@x#|&yIpvB98@vBEV!^^KFLvsn8;-?;PBdYaPWle7+(W#pp4%60wUL#)9N>q09 zzK#JmCUK3l}!2?&S8!{PkeM`1yb^NCrJByZrjjvm) zFR~n$wUcQYd+9U?6r@!rwS4Elyg%2!C7G1%I$;LOEoJWd5|emeq$s=MM~?3YyNWV$ zo?bqk{@q3euZb)F=>=E-BSC9K+;Lo=umf=T$jw?5x-uek{BTRRyzT`zgPt(VSIqAA zEU$wTA&anbbTJ+J^X}>VVlcX6PE+JFhT67B_;~LUB}#U5ynu(c))IrBaG>k?Tqopuk}-hs z=OFt!%EU3KIALBoXMlM%wPVE1xsOr(U_tt9S0U!hs`^H) zjL74Yu+-{TpGd!#yf5SSb-gVOUtY-+S;XWKp&ukCr+Bj}`L%_ICoF&PZ`vT|kAjhX z%++=B#N)r$A4tqJ=>MG3twGOAzT8+@DcYkT%ts6AmmphK#-xn%zdAroF+>J3k_4)k zbTYcB=(3z{OMhwU4;m3G9q9al+xX{5i3-!+m^=*REbYIqrB9dHrwiHDo2O#Kb7>@;VSCchs@}E1wXVm)EzP ziMAG%ee3qT`%fo7LcRkSM>-kCANn(Rad`X0E9(KOT&7$;egWmL4qY%J zMVrw5ZLsCA0pqZd(Rs-Dnr@KR{aP{pBz9YXRb6Rr+V?w;<8~jN9!n?`?T4ewKYgNd z{bKk>1CzEH_X58ebMk@@DBF;hPKNKrQ4kSzNm=_M$xK6%{B;RaKaIkpuevVsnJB3K zxSeMA{5tm-Tn&@bO`4s@i3mkEuYNC&1xuidBn%32vg)Q#BSbN)7kPQ5Ulx4dh)1}Y z5T_}D+XZ7gUFrtft%qhCryuIULi6|MRC^5?Jm-YFc94{mY<^Ti)scCTjUFoHag#y6 za&jrN4`MBcZI=+0Y)sJ}Tfb@c?EP_tr4u8pkBNY%p33lBK$bk zPtLmi0vT+walV1!=Y#vY+(n)~EPyq(?8| z`UM?9-sLf2gl!kAevgu;*QPJ^S1nW%6Q|^BWnJ8`i#{cC%RIZ*%#X1z)(yX zT}L~PjF-QE9zou6r%N}ZXlha2LRI8=rfrJB!i7vHJW-)10sWD=24P{(jySw|9;6P= z0dc30fcQNiijKL93*tE~Ne=V?dq+pX3qb8~h?l@ja0_%bJ|V$`5J6e262zS}UvKW* zfZ(*o=Y4^{4Ps^%_Id-}3Obc@KCK8qIyX1hr(Z6;qK|@4;~6>+(6Gst6w)mz6~P&; z@u0|TIi|fFJ`)(yQK{rbU8@j|sp_qDP*o^qwC~iZhvsxGF+b`x^F)Of^wFb7=`*4Z zjmiP+P|T2M)Y&bj)1z2rP@0(Sk_6u5iO!U@8}4EBvg~=CiRk8!qnXw;#@sslDHSk7 z>6rk@&)`wPMe>ruYzUB;*l+x0x@-8o@AeLhtw~8 zltTf&<6w5C&wUQ@)n>%hX{juzd$ zTg&6Iy1RDoHcCq-tx~ID4xs5R93VS>R*x>w*&#KTs#6 zDO(`iUiWPiiB(zUqnFo>>2n74@!cmQKBXN_-~58gta z&ckB<)PaKlg#f#&WW9OZlC##{s(@@+@%(w8 z(T~l_o;{<2yTsiyZ&KbA!%g-v?%$$8Z=LJl(LWr4_|~o^6lBO>#Enbog={EW+RVTt zzy1;0#=NJ018qoBQd0A%f5@#ZmXifrihU@j-L;_c^Jf4S`Xdf#?w|w!F-2be9@^Mn z!JKeoM+tFQSN}JcJH#g?p?&q3mNRpPgEi>-J>vW8do0rc$%7TmS*aDe%qOarIpQ(| zJr{;}`k61?71QqB`I-rcjHm$hKgtJ(8IYSWjh<*Rdu{#8MR$$oG%)Cs1ztmP4-%xf zK9X`OhUX30SjHA^y!F>)|KEP%O{XvpTjq3tn!Bs!{#k1snptGLQd8aH`&0BBx_kox zna)QK`msIET^``3FEgDrM~KKvD&@h}&7%a<0Zqk~)XrhC?gRvyJJ-p|w(w}3mpCtspqsyAm&!r8O->(-qD zPT?xSCMBMB^w?riT}WD}_>AUqd67j0j=Wr}p$1)j1&K4`vgX^^yrf#l86cN+_wYbj z>JwCt9{sNx6y$|Ju<;vp+US%)&eefgLGP4}EmmlZpTQ}vc$&U(>1X*VpTQBA96K^Y z5nuL=NTWzO>}sX_wLA2jhu0e4JIegJqqr04m6 zNE})QdAQlLRKd;d-HV0BS)=ljVisR@uQaAPuyySb0tg?gQ_gUoP8}oj~Q2ZfE=3j=c z7)|?8Z0BDhxBaLu@rQYxexw}y;aH!au_g~O_Qz4=UpCqPtjh7H{{_FOZ(;{Nh_9)Q zA7AeKRG?$q^RN9EH4>HhD?Rlek71^15>V8Gzw*=hPL~uv@}F<;gI#x}J3ARj)uPDQ z><}!;qG3FEMCTtbIEMfYb2af)>-OCG=}P|}yxx)%PAw%R#a*9LY3OfWwbIMxk4W3G zn=&y@2M!&Qe@s%#lBp>xvBNKooQLhvzt3! zI-D&8>?J|&VJm?=Lw#+r8;04|R4m!Cev6#`g=+WTp0r1i3&dk>+I218WP*D0n#YHm z3$Ene#X{VFn<2t%4M|NamYW&u>}sQ)kB ziytSSx0yP2oNZ5u?n`i7dJ9SCsutx|ZhhWGCcQ-jX#nAx%};wL%|;ycI|q!BFifQaPC9fLx+u=H7ngw*1)%F2!h#VR2P4c+Arfe+oLTOzp| zy+dAa30)Ye;8X#uL-Ql*Td5Z=KpZK0AUU1#9~&HX`_2^SFT7DCf~7`uZL;|K;U#Eoj8} z4l>}H4C$LyJdI3QK-yFk*b%?+;6!-de5h-hLwceQX9$k6+t)YWHn9#7$Aj?fP+~@* z``o-2U`d%pq~`as`Q>t)q4Ho4_#*Dd&M9iR+qEpRp3%Z%xxOK^|t z7YfV^QUm<`3HoEmlBN$(7K}GseCEUP90PJ@+5lK1*om3ztBLl6wTEmhNC+ZWg!KQ1 zCjIecg!7s^1U$$m0ZUb_27LWSrHfPQw-5d#XQ!Dw1<;uailhv$>4ylz%KYI-SAzkt_gpzBBuML`RjZ0Gd$=tL+23wMl+f+wU6^wcF5 z!9OB*Y%fC~TYM2j^bkr#u+j#Uk!|L$yc*tUT2vr0Fg@ zh>Rj|5yu(1H4*6%r_{}(Don)mhPjDHkJ8&9iXLo2OY=GazM|J7S_WRvKcUsB(Rc8q zNgj~&RZpKwOcd>ugrt5au&d&X1XVJlBa!%@N@DjI-DQsC+lM#|M7sv)Io~F_PcV-$ zIsJ{inC{c^>gvyrB9>kYW(ekN+EaY8t2HJ~>Tst;5-Gt(pL65t`kAD90tvv7f%@@4 zsw}>V$4rWTf&2EMyFX0E>*c%hC1Q3<>zB3VsP}>MYWtx}0-ZurlDh!FD?gv{9-zlm z0n=sw%7OO6?hJhl6^$nJP5T$MlOcWNU{ADtDBoyL*{4oZOk|o#Pf7G&n2;3l^LOsl zP-7FrIo*?49ms%-Z>=b~*vMV>kDhCSq)c3w$)BcPo};H%3bME4)uXcA zh+h>G%B>eIddeBI{=)x=<8C+V$CFERUAJIH-0I?qg)f1B$&+?DiB4py{_!sq|1=f4 zEe1X_K9GR|gKA2$jKg&s%`&6UGF$WNy?bYDey1Bzw9||oi6%GZdso8)n=w$~<^qiE z8c@w+lHx3NBT?HEbE`uA&fU8uEdU!dLCo6Aoq!W(7_VSQ*KbXH`p?Ijb;Gf8Ap`+z z#?n+^qx6P#_V!#K!}#s!X(;!l#`kbTJ0GoFl8r*&UAt6IM$uhk-FSz)Q}s$Ph^_xF zrb6cJ1Q_h!bxRNE&Wj2W{PNwiXT0V9j18`o7%{jlr;RZ`$KO1GdFcVpC+KWc9LltR zK1=u43ruSjR*svfuHI8UaV}L0lLzA&JtI_F9&=HcIQMyOfo z5*)H;(^ebEd%b*acq>zTNmCE~fa1BjQ;r@Ay9h>n@^au=B9%zgzoHhjI=wwJElq2~ zaX{N@Ygf-Ojxo=>LX)6lF9cUP3+|>7H`dI=JhX9$75&UFC+}KR@iymJ6{@ z95!q?=tCD=n=B*gzDMvR8H@<k`U(CR*#M3he+4{!0N_VyI81MLI|_-L|d zJ%Uyeza7*-xi9W1_xv{tcpDrkT8shd%HjDei8`B@2&ZYDR{r_oV#>GRCi5e^E+A2U z40|(P4Ah_=xG)K{;Mf}TcjrPp2H2tF^ zSm-!dcJOEyzI+KT0cL*kjS%xQpFh8y%m}7jQ%(^zy#9DN<|Pk0zv}(lw~`1OI0c1n zwuE7CJ!JCajTDJEHd`k`-)C}>c@h{{IeGu<+qA(kAj$docmNvV=gFL9ynvdR>Kyu} zt>ddvHFIO4qy604A#(zl<=y!*dVI+nj$!ejUG7qzYPIaAZgR-7eA=&#QrjLvLtjLA z)HgMEur)Lbn4Wk5I8WgTs`GWMEC((U!+=DJlu%&>Le5M^TnElFCQx$+kQW{BC>249 zYpfHb(tzRnM!yH?@@m(ueT*}bAu=y zrLmc7*T!YP#T}fakD$Cfv|}pc9s@VFlY#RlH2VOiY0S5nKOZEZ2AY*<#N-lNZwG6k zTJfja_+i7W=`^UzU%y1dq4wdudv9oi#wD7^jDw&6>&ZMO|K?3u-kJi9Ajyh2Ld4Qy z&fx8tMh0Lf2hPjtt}MvB8MbboZ_w-uX*K4%>+HeeXlho>|KT0uCXhdGvThlwU0ZzM zxIS%@a3gCU;P07EUJq+WO7r$6?E)BuY}y8)%mS)U@4$@sgT0`!^8)QJRn^oe_hn$Z zAiIq1ql-TgBF{nl85v65LgogJA^>6M^=3#~kva?N#7h~_aqa^I+m(q$tV2fo4p@C1 zdi3Le>uFsVjNO2DcZ;|^*)m1!V`k35l!eX=4YzWSzkg)gz<&m%nWM%={2iB+6ryUa zO&OfJYlr^rB_%&WdG?Db#(sBjypP`_SO&CtQe5oa|0|GeFtT?%srLZrNk@o91%3z@ z_~ihk>yJ{!WM4kq`Q4T|s?g6rD916>>>ESX<{)I!mZaSyRH`%3EE^WqXwYNA(6%JrlF;XYeqEF zPKGwgrRVWu(r%xbDlEhXDGkwQB}=kqZH>*!i`2hfQ1CJNMM&<32Ylm3@M>5!7vMxQ zcAQVU7_{106vit3O-affsm)W)hM+;@LxzA_$LG(VrPZS`rwD1o`;{suNbMpiDS9O# z1Z!(6%F8p3@<`%?WEx^7H!C;Mjd-aN36MJMbCJ5rTlOl^p<;Bkzj-?t+SDuLH;M_jd_ z*xfm%@CO%Q@sC%Q7=YvW8?ClO0ORoP+$kx`cv4O{C4}dqE4-Uqo?w3$i;sN9meaYQt-8K`sVQL$+zi+4eQrBr_NvE`05{-$FMDe zoRCDWF6uq}UF$E(9Lq2Iq;h10)tAO6zZI@7uYv*C{R3?bKp|O96peliT9SRx&BE$R_1Uy&1lMW>#*5gmPIPq0zy zuI4pluC@nT|4~9B?%x`U{{w`j-)^dk#mZNQ@6J_}-pka9LxJHJ>)*YLKU-cf{Mcm( z$V`kBC8L%vMM+-!e%8G^A1Zc?HyZvlBC^AY4Yh^~;myOmpONk=A;kST+pTCDKVu}7 zo%8*N#h?CL1XuvGo6d&si}>R0xH8LW!9m4p={A14f{jRqxKvmS{-X7R{Wh@AZ>?mL z+8=tPIJHJ`lj%iN$;B`Pyh11k~uNw&8xB;S;!@)P+tPjN=ew_ipHooL7Gc6oao20ZqCk;3xLk0G0%Qopz0L@U=lkLVIc1amXHqs1(hT@HTCp@ zK2*LY2;LODB`R6~ViQ^XBs6{FfA<0a;t z_8nuh=)yVkX4Nt)R=-=^hW_(TBI6H}#A5tugzA%^wACO=K%G~NxKT95gee~W^vU_a zJ&?{4AmUK>(bRfrLE>d9v$!_yd?Sk${|Wt)rwZ%fGJ>@AEu0_Y8Tj^0ABjyXkO%Qw zkw(ih>?CUecC~39==QOHuNjF`Ha6S!nLWsBs{HW-Q(rEK(pJf_kO;T>Gj@|}y7!>| zD#wn=9}FFA|6AW3HXUc}lW03?z2u)0f1RbNv&Bm)=YU7kp`-7!H)Jh6cP`^s_q3*o z>JE3a)kZI0w*198hh;Y;S43`y-n0>MChk;E%?ax2kY`djsjenSVQBQA35-@{1urv3 zfgp>NXh-}^O-HsH5Hx7)Z!%s-jy#6AL(RUbwl?J8!3_*{q$LN1h4wO0MyG_DiB3{*}sFp>wl~^?Jh3G6sASHlpI%5_;7&4yZAKkmt(>D~Y z@D(O>HNgO|XF7o3#`@fZA|iS?3o_X|xrOk@ICNe@Ar?x3(wbo8#%33CoS68HlIF-> zgpLJh-|!{lMc4>DPkwsNv$=_OB7|cg8bNQL5H%e3#x9X6gOCbP=EAxq%wWXcXo$u( z#W%6JuQI((vs=ZzI(Cy!1`nfUd5Bk}(WxBRP;;KAq^1gWKy87=3%*3{XILN+SpXbi z(+R@cTR0JW8-#-Lb4JGnTEHo#{Y|Heqi|&R+vDLazn{8Bvk=_oAT4-wWPs~}z_rG0 zB;xv(O*~dXhv8m7FxvC=Yle3t*^MPhsAH&*Dv<{`}aylS3$HIe^cgi zojVl4=j2#eFl3XtWpDO^7D_z-zU;?-<1@W>u~^>T2XdZ!-rhgyefDUBa;gpK`pT%J z)Sp$i+(5Qd^`?xYP8-GLbPY~!2UN>*F#MerI$%6k6TlZ8i`)3k)k3?<(fXQkCyA*Y zt~pL^hpQPfOe7ynfLK{r@hT1cVsRuF(|K2hXAi~Kiw@N}>hO%6+HOg-lRjh7azymT z)5)aWd{8<3$3t%2TJ}N0@7KB+f@R52Tdn-cDQV0mqov)da%>jwQ|+f$fbCQoI&>&J zDRxCuk`DpAm_lPRdB8{&d8_l0w;C&6Nqdh^dM(dWFPeKssaveAgoL?NY#Z%Jz%GTd zSFcX**X?~2+KH^-9=w*_{z{lGyvVE*YO}%8;lOt5)2E2so^F{VU(bJI^9-)td=)@G zwq2a>kr4&{?MyKl%Hn(+hn*JY#huX}SNQ}WGc<--OwO*N`4#xWVB+Axn!EB`KtbHx zl+HnQ7hTb!*~)(LUuP_6->fh@7Vz)erC0EeVMfcAo%CDl;9!{X9Ng#i>!5=N<+>Ma zWf`^(n_@2KwbsdL06b(038H1h%b6IPyh(IGK-}pd1Gx`gR*LHf+eXD1mnHSnR_}Yk zed2>i6ju!Yx21WzzW+SU!lvJq)FQek5J1Om;d~)XN^cO0E0>PKFIXmtwgo*5bohiR zaMq}i9Sq1@k-WO>FpD@LV4m6$uDdv)9fHWCD{PgYh-GM+9znQ7Z*odZ<~KbQ!pbvjy8&9rGq^8d{e z`So)%99V9kX6j+Lo^|PgR0gM(0kUYG8hO^}d5psIDB@vDdH75tB@ zVF`FnaN1I#bpSPxb@0)82CHjCI7R7xQ0tz2;Na@*BXH5#i5(W5CW}KD(FT=rRuw^w z!g4V-SQ|9$*4M_1)Pe+~Q=8H9Q&X<3n>C-~|h3`b413X&J>-N2S%}=*QW=Ab*7+1VFS^#JWlqo-zt zwupts*+mX7!N{^JE$A)v0{Jw*8G?#fLv(dAEKE{{2p!h=(zCY8)@1Y9-;M2+)omMV zr)=sfudXge{MN-iXAR*4G7;c8*htvbZeCKNWn93Fr-xvHw85g8(=0e^b$+~#3v{*! zNg2xJmr+J$Z2f_95dJS8)0%?=0%Sd+afrBIA-VAEW~Zu|+Zp(Ty5yX4_o6JkZ`#hU zU)>wq-sVUnDqyVos?`Ntne6py)BaivolqS@u%~f2O&3tj>S_M{v{IZiZCaziqnNoH zH=!Ue&k^=Fxk1u6Cqr}_wh!EBGr9hN)U|?w4GiGSw~mey?Q}36c|5lA zImUnCICb?e^mPNXr7*CGxdjxDnU3UeZxW)L;{gRjmAKs@u3I}5wEGr2=R(v!B)UtN zwUCN9C!{pv=enA<{J)m}`SfM*zi`-IZd>~=WTL*;9Djb}rqz=FGGs1A=Kt^?dOLnB z9xm1sn!$tAiSmDtVUjtdIMt-s6{<4qIS*)$L+0w(IK1wCU3kUtV5smsZ|fL$-lye zRGKi>T5qbq`h0zVw3%RkJ?!-an|&pY>vn^`-6dX)J=JW=~ zexe_ckf*EWaI?6lT%_;6Wz+ZH;<*7h6m=_DYxD?VH~2OC6pbQBqKGOp-PX1mAN?U# zken9%L`4Gkg0 zz)DhQqeU==%SD8ZN*5nAY5SQ`YHBV(=U9_uw~wnKP2d(8hkF?E{fU`f91{q|?I(^PxM^CA>2XS2i9dJ@N-h?0}Nt$=x2tii6YO z9-+)96*?zSm1@HXqe6CrC9Z@BfSG*DBto)8nEoMjg&wfuNudg1HYu>{=<3E0Z7*81 zsHJ$lW=)nkBB=QO4H#*-TLf+>Z+Ln(qvlHwf(BEV+HU#tk~8i0t^6Ix(a3ifgEnX; ziaLnPHnG?cCZl(%%L`{jOnJ?%jYIZEd5sugDr87MYNSdBHYMHP>U+-sgp!gp4I-y zRCGED6i@Hn-DhkyWlIBBo`k?yK5oa1x)Y9Q)kf)^TrC|{ zj`s-LW>_dpnBa^?YtEu0vLuE4%Pwf$W~VL4zW)BLgK%yLw2Tr2a-eO78?5zjo05_u zrqEKr>7f~Ip17;!=xXXrF_DhL0HmmWv z#7|>3c`<+63Ku?n+33P~B);f)3st1V zbdxBfDype`kf`#_Wa6~JW^UU;7~19Cwo|}Gvi>8fUkAH%#Dv2y2LMASpb)KXm25w3 z6)Mm^;3YPcveE6tkkNeMFI#Vrt~> zJT$O={q@%|Yf7FJ6qp8%9z3|5dnW{qHfLFqol*-QoL)`osw;UmN(%H8R_-_q4}^}* z{prpNQJm&v&ROh8OAOJO3gNRoV>lLbHw5`cZv_Yn+ZSF>c!y$7?7}?0E31m zo2F>YHi*8PmzU=*rQ?kVFhqFy^lC+uXtC~mfdna@Hy{oqpTg1&nfZXYUuetoEO=za zgVNN=;V8UMym9Lm$^M<8CZ^;WfMZmBvWVixAS&k5fxcGipLetL8629CZ}zA3URZkm_WuHv9R zZrzAay;@v~rw+$;uV-IT!p#b$t>M_$aJXz*E7cHm7`mLEBqIneE+G)GGF%9y?~F!3 zif$(JySW6j$YRaUPFkALwQ`6RTcetBj)rKL*zVDNkeFJ%I<_%~YBpv-b~!O#*z9eP zl@!Kb1BhF^u7f+W*_n1?*4OaLWziUq^zyEiZQOxROfpW50BQ1dzVz;9UqvK8VP{vgE`|*oRv4cGchPu&R?hY7 zLumf#?dr!*-w9U#Nb5&HM0Us|b$majUHZV1|2ad zM8lSx>EkXe;pZ!x5pB)rRgE(0~-<>j4jkFuuyW;rr&-QqUnIk|mNFmxy z>M3~IUy!StEJoJic@5`^o`kPC6T+#qDp(aWJ!4qq6ROFKc2OwC({i>fqP6h+L;|v& zMx964#t|e~ zv!^lKTBIDfMI+TT_5#(b{O5|-=gN-VvuoG>gO-T)r+fE29bGBJ=W=CL-k!78$R;;* zp$K_E^^>Vt=3g?EHYS?^T>baybOVw&kjrNXF}}p0%#cN0FZgfSp()wuzfoE&%UdhV zP~?8YWX4g?AcxwMQMxzT(>h@KWNB91;`?DfgC6x(@Yl~?r zL>_WbxZoQ03q2R-+fQxqFnlga^QtAMu)zLp6%Xbzmk`i%rJ8Yhbf(sf>C+FT70w+; zOntOD^vQ$g1SaGxis7^@Mei3+FTd=2OkytlLXj0gn~^>9Nuu_cnH>`pvUf4?s}xJu zF7wg27A93r29$pm2>b*&sA5+9gjpvmhy?9ZfLN+OtaEZ2R=McR4=w-$e3x;v_(BTb zK3-hmqjOWi`UuEe)>Y0rgt0QTI+%iopWW`mi<~)PhL?3Lb25U2a)ytZ?!z-yR&(hn z-`!t*+^}Kk4Smdqj313!{FfXaU1H|%Y_x>ezOReZXgDCV!nG!S(hVVR$lougtX9vFD`9 z$r21gHMh1;Iv52&;-i*#RMkiy(~fn@9O(I}Y&#(z!k_94Y|F|GWEh~qwTk0fJknIF z5#O`8w+W=k$uJiL_{mM+W)N~OL|mO{OQwTBO>+T@C%QG*ZJq%>SeZ~4ZX=uE^@_^| zhF+ZYhvsg2C!vQKxYF4T*-KxDdfY`LKZlr0ahk5QJbF&b%DUP;6)l%{wHiKHXm7#+ zukiRtx-Rl~v-oRSS*JO4co4eQrj#C^_PhVg`s|E5*|d|)nN$D%y@UHDVQ#JsX-yp0 z9wIhjHsy%n!{_vtafl|yRqNlQhw&r*PH5NKtqS-$IT=E7=Kg6m#JU^*JnVr3DB#Sk z=#>r3%}Bh3YKJ1)%)I8e#4&VlSxUJWMn?pHaG`4znL}n3u}%^8u)awX_Bv!DMye#U zbxFew3$p=G{t$k5YP+k8TKj%~M~eql+>dm`5+YlVGFd*dm(aU_;790hGk>4<#XHi( z?B=^H!gjm4xViPLc2V(;_SxPWw4SNh;VbD=2fJ5Zsc&NOE!@L(1>$sU$Yf%5V10}1 zx0pQObi#BG`e&0mp0({7>tHrRIGt6_DJ@MN$Be<7Ffh6U)+;eggFPc%64M(vB;%0{ zL}N`vYU`Eb=}v+nwHwu8;O(IbpLULKHx(4ZyvP=^5Q5~j>h05U2}b@iJ(!*RLkTP+6j znDPo2#sP>(NZ5?Uy{m~8A~+fcn!~-7PS?5XH_e1&Yw~Q<-p=|_MhY;%kRq4ydSG)A zoB}3Hz;A7T{WZZhPZ3C2Qs&ZcV6ofEn|-~QQ)0F%v1y9_&9JbcJ~~Qb47h0fC)^}* z?fJ(a?}*evwcis=gHat6Bale41S3$fLo%RekjYICv5+34T6rl`4{!mtd3oI)h?H6! z5d|?!j~UT(CH3izRPzAM2{qf=-!BpNv%*MEg7|E!;lBMR(nX^7jU1w#jJQVC4;5Bu zb04U(9gDx4>FlrSYkW;jU7exM%Sco3rVK9ABFwCq-+H^EwdY%prbEO#E`Vp5`q%f! z&ZaBJV@laW4lQi0`I9_h?gr!mdCi?WP~9mzV6X`9n&AdUTh}B#tcJk&+jA877T2U< zTyiw7w;1L6&l4+8ICge9dH>Fx7$SOu&+Xb5C=g*%O$dW(U)TMEkIIzkRKHeKy1f>g zwf?|3a%NsD6tV!|crCiMc3FpGLiaC%SJ*j)2)t(E2 z#(m2isJrl(jppi;Hy>z=&AIg*X1YIJEnvy!Hj4zoCP3myd#YN;jtx>^z;`or!pIlG zm4?jiCQ5wJD~+>?V%oKDPYQY|9zem64TL2+>QPS`#;;O)b6WklE&Au@mvHRL$w9#k^DYd-G81Lrp-lN6LMb&%o_ zD@Tw~t7e}5pCDy@Eo3#Wy1+QYU?>%;^1axO)s09C~Vqm}7cDj|>J5`C9UfxM?@ z)Gyg)ysf5R-U;;_)$10^W8{%#E6Lgs$EY`@4OIpCB6LXkKq z_#XRbp^d)Z?2rKno)Mz>_8xun;%(4tfk6-%SKeUYA_nDD^zHINK7;`S(Pl*OdZoyv$CTZTRfsn+wWfp56zy3f+3Rh~@y*>+pvZyoCVRDLQ2-SOh zRP(z%o(Lr9d}i9m@s7oTf?dp|m=mHb3>h5Me#S2-w)gJXRzcPksw8G&qHRVWFzvTE zi-S27X<;aS6?3H!VmcrQQS|FWtvE1QWJJV~`2nZgbN|Ao_p@9gUcJqCWHO$!Knn6W zq?GKjbAgyi#ov&`?Vaq~cRT~9mm-Z#A=o<84^{Lj)Xp(h6>$d+9xMRG4XZ~!iuuwx zQ1j#>nmy}#x){xtxxr2fpLSexD@(R%lLX9?6`$eAi)KEbzYo#GPm;vaiGd=_yHlWI zj$Ry%XIv=%HX3kJP-r~4wOBRjw8i;aGCm<`R`m?3FOZWMI?E%Ql6JE>tnL?VZ)H*k z8E60&*e8E$-(I7x4YmA8eOEEen5v(w=f6lEpm%ps$ zLr7%SfK222JGGt01V%If=na6UQE9-djBHn^m{FEOuJncRS}VSBA2(@!k7@4TSTvGf zaEd3+nYBgh#5;Cj|3Ht?Eg=OARODP19;Q#6uX2t@$CTSDM%^@D(+vl{Xux*wUO&Hgw3)ZCwTecHJ zkkBoodKC`g!LGcP-%S7E?F7>n*$agw=kxtktTuu_4BYtXxONCFmSP&)ik8DGzoMgb zX;CL}lbx%B2_#(~9PUqQz-_cq9YvW0^Dg9T$=xZu1&QR6XJz=^MenTUzU9``kr2)i zrLOu1Q2xh`HBOF1FTVHaWw+bdwU_DGb{dD09OI6X1GpSI&#u5%nLUf~VdLq!lb5#v ze^$JD4o`MlKZsZAK0&J-R``fvz-PIP3|d)8IbMu>rx3*;FQtgzfBluE5`_+t5?SrD zgT)Ja73jAR;QG2%+QGm;(E?X>XDg?*5q5&|vU48O;7VWw=4%(6GQ3qj;s9~I9D%hq zdVtJw4t$Z3nOTHA7+>lxAjJc74alOP|L&nd4I~_!Xt**=O8DVXi@l_XGGBp$oq6ru%jsIwGQ99=JxwN@SF5;F?U(@L;PZ0vmo441m35h{( z8|8^b$c}X1%`H}mZ@E2FDSY$O5>scdCs(?1l29EyX!=LYD{fm(mL%TL z4e({Kx%Wc2DGmflGBirHFMMRM~5Cp4{ zo<3;{5vjsS0;TQSaT_IIcOa4Ub2*{s2;n)E?Pai2-4=`Gme2T}J8bI8R8U0)1+mW{ zp(#dB2{tMB|H*A2LO-SRTfa)4luLpq>^;uzP>G{BEf2p3sVz^k{`&W1Nc`#lXXU<@ z7yMuMP8z{EJQzQSme6_iEj8;kG>)Ep zZ_Ai5OpQpomfn_fNrRHO{+kZHSC_{QZ%5M`Gl9PnhtfskS(hPaE?s$L+EWEXjhP7< z4M)eVI-HOLG!|;HV)onf-=3Qj#65f~Fw{3G-LK4e(?Tm>8;AMdh{;8OE-v6WFcLwa z`-|WWlq4*IdgP8!Ru=79nmahI)e8>FUB{h=D>rQTGQV@T`d^io?}0vYYWVAh`ru1P zFH5W47dL6Z00gNuQ%66Vd+Yl9Q)*`fyho`Ca)ICElnWPHgpBqbJ*u(>5w}Rw5869O z*L(&}nT&k&=R*e%3K`whgU|o@CiIJlC`f9snp$$CY0R0ln(+nbN`-F|N|mk!;S#O( zM`IGo)ChrmL1*C94J;2ZqIfx6F^oB@>0?pPyGN&7R@S*^O}+tV@ApsbGBNba@wocx z(Z9uh%&5u6nc!{TefZ!(=uwmPR9>%k-0-=O0kBv8OB{JI0Jog;+Qa72`b&hOc&i%L z4_`G4&Exz3GA!WoE!oEdhxKm3?4By@+q?HZfh_scZ9~c{<0wWFUDy-4iTIptZQHTe z(~A1$jlYQ*yU3T~1!m|662~FKz}KTuclZ3?Jtp6V^P}Jh&pAnD%L$F-$8i$~$yJC4 zB=rfN*>u7V&XdU-ZT|@jv4K~B;M}KAL=qbG`QuuhRAL@=-rQBEbjg95J^OcG6?5@6 zMf^!(Olc+7GI_9vUGB*{GD+q59w@1tfw|ngqZ|^>SUlv_!}tbhAkK zlcfIMK6leE-=GL;?traO6nuQ%1dXx#Rr+HOQTEjFP%>PcVX=Y^C3ZaT!kAkFe$fkD zJR!8ONj{_1OzUl7rgaSvzQJdJ=7kI=N%Y)JZFUVFijba3BvP7T;o-WJs$XiQ|GG&o z7=Hyps6O_$x4+Ui)R_U*4GpzO6npe;?|t3y1d}Rt+L0Z4tTfoKwpK~)tCVwQ_YZ&E zX_(pWR*UJo((MgVi^y(s2(`ZAKTV&$9D&%ICrNTxJ-tNK#f=k;k@?U|r4@_XdIM8y zn4osK<9+X|i|(hk*ORER*g0*gm7S;G+7`fblUzw!^S3os#x}}!nEbn-bo5^^Nv(YYddc0ul-!%?&6pUzt*QWG+QVHE^q7D zv1PcSMq%iQ6DYfPQ4o^BB9*=QHx>Ldh~E-=dr1K)D5}A+M2S3!bO@4>nA%11$2V~T z*-f9>cd2z>W?Hr84&hP_Clk1EK^C^~S-EuniH}ShIDJH;k{vowq;_z0M8n_iLFf$P zh|ll0CY_IrgH4UcU5C@e;CvDZZ~@tssfxQ^C4CZXCQ=?^1SWLDB8x%_zJtoe z`nH!+i2zre0 zVs}wL)TNrWbZG^CQup(;zbqypt%i$jF9W~mS#9k`k)0hG$LD8LWkqTV+=S4jFTS0d zn@|ul!R;E4bx_$t*6Stc5~<+u=Ol?@0_;{P$`@QM>GzL{Yd%zO?E3iL0M6}^Np49} ze4jKS*BC1!2yO(UqE1HMg_sHp3}u5A5#wHzji$}Haq}jl%_@oC;3nXfMb=H>XZ`+x zBNt7Dhrk;npf>o0irsSWojVHw2PmzvsK^t*#1J6^&|eo017_voWb@9yIy3}d$}21x ztXyhhZf^9{b=9du)118e2MZAujP~9f8kMLh$o7UmfIRo4Nt4)^RqNXQwoGd4n}+Ax zBbIDfy>8?6B9FOLB%cx0DC<5U5`_Kfa+Sz}^4QFl+7ieX-I3HuI>^8+286os2_tYe{EyFS>*~PTi_!Yy^l~NszTc8if zfe}>Vnn*q7NUlSIjWShxnGz~gU`4j2A=+Du_lPPd4Wv@#TYD}%G`mF*LnMZx3mYp~s9QKcC&Z{Z68KUtlwaQB3T8 zo`xq81+tMZ_evLzKvD!M9IvEm70m!B90=6Z;nLX|8S_cKgjSy7Bv{1UjijU;4kt(d z`bEF1i#{dsNK| z>hwnRdJJ*3oLq-Cd460=vUR3LITgfwa=V7z58Fv$AU}Xk2IWdT;{mnbwYpF zLZRY1KeOR(08>bi7+EiN(*RV50v>dPQSWPEP1A1uJ#Z6JAT6 z*f#gewGJw`3vu?@qg&K~N3DN3Z)IQINn1RMOTNB+c-diCdUL0hAJx<-U7o@ zU;__o>*;mr*wIgw+T~-LF`4=k9qNjJB`SBymWHL5YMrObu%=^$<);X*7Z6R%l|Ua{Zgi4AT~Yzj8|>1{&QV3wZVd6V}XxNo$z+bdZ3>aMi9 z&=B*$E3ro9s1cFJ!2bPLjcUsW*Ma&>ajm~ybj^F!XvH24s&le7#Gg$92}|sC5r-|V zwoZhZEftu`OBPZu= z14)aJtCEf?o9KVn2^Nz@NTC1qvBwU?5N=4APfw0)WI z+qPF>Zg)6U!HGU6_g}4Dy=J4cZE3Pr@a4Y}gARZV43Bw0PdA9WY@VQ7%%Pchu0lBD}Aws8p=~p6)cj43+0yy&qwF*;2D!vo$ zB(oNNvr7+Y*sJtzn`o<#vs_*(4vWy9ZzFRpr(GQGBk8Dw&p5AF?Gv@S$uWwPF6vW z>^quwD4=rxfjIZDG1FS0AYETtHe+32L~FVG?a~9DD)6dq$Lo|__DKC|7zSe;lyCyF zGjLit@0^RMevWwZU|DNeVoTo5+^1}mnK`93qPTN%(&qq>lxj+*sA7@DKBeF|HRzAU;E0a_5dRh3LiJ4@VP%7>8Cly|KJ>`LD4d z2P#(qJ4SZQMJhsPLHhOU&*;PFyk2d$RSNpW=)mx}V$ZGdQG8q)IpLIcbGwnfI&zfJdpQI6o66P4c|vT?>@aYQ&AfJn zyPYhzt!R9{KTBKt;D}3{GlFY$b;N)`V$VAD2gOgAor(B}h(=q4>zV|T$|yeFo|~Ztnzft^p|hnO z0w-T{6Fi%nD3Htc#(K-`>0t;&LoZe3lhDouvrA}ze_t--$H$z2WrdoXS*ja_;%O^V#x(73=klIh`OuWIrFV#@A)tl-El~YHMg}^n z84Zfw{>o?~S~njn%B9oBas|wwy?Xb?5}GHaO;lD^ojBNXxs`XnuCeEq%(SUWz|zh6 z?A^F_DZxFw+^XDLiya=0wyA2E@dv$QeNC3U9EMUB8<}J2GAo;`DeGJF0lmqitd8|B zn|P_nCQBh!er}f5<79uvG&&(T6WVE|q{&t%f z-uE1uVaC#cfVIIME5}5I{Ou=~r)r(a5Y;0~sk|FM*VihpFWTL(KpYkW(1xVfkW%Ng zpBpqm|L&rZy+loZ|DPKNKgq~YfSj{bDZ{$MmzHBOAeIq1e)lSf*9p)^s~G?DRLDId zEv_gXN7ibZp+;;vB8rMjM0PVrK5I%|wx^4>kXTN;{uL(Tyt|6q)-E94ytAe!okrw* zSuTES4&8q=xCdqXfL$UbTfB{v;}V}ae(>!RXFh%TqO-*?keNYSp7d+1kpvmAKhyw@ zygmJIvSRYk{aK1@YP%Po92r$RIrXu{pvLVfaRXlD=GazFI*bh6{l$d>diOjwV63gt z@#>SNdiU**+^b!E#`I&ZeM6k{$B~e}2F%F(IU0^Hu=O+I1B*XQj{F z!C}fSKg5^>m$sE}Ob)~kPH^{sY~S&)*DFHEih|jGN~X1_B0Qh5tG=({)}ljE0_4-a zz#gf^G-Y~;Vu{fQhPzrfLHX3#32$9#<~seT1*r`*f`*H1K4Hv_&IA&Ezc0`yFTH$G zfvCEoYR1D*Bxb01=v1-f^Ro@9NRXEy#V=An5emrCwL+M#UB#aR(+d7nMa2oeuEICu z?`G{J!>03#9RQiJa$}iyr2aq56f~s@Y#H2)$3ijf*KEMAxEh#NqMmgtpyzg+5x7`y+LbA^YlZZ0m73AwSxY(DWh&9K8oZ;&VSJ5g*Uw~_HeEa&3B zM5T5S>%1c_guW^>w;dMmB7?1d)u%STb|Z`IslkvIVPrwnIqw9Su~ zo2s5~_Eb&VK!_z4s&>mfU~6V(M)#^;zV&F^n&-PIMRJzsKzshlPYgJR@YQ?*EUQ(#T`I^5LiEWVD zkZ@RlC-)m}JaLoH?4Rh=cYdnFi}Lct1CocQNK=@YrOlN-Z}Z@qc~W!J$28G<1d)^q zVVDBbc>GpNwwEFNSQ>K-FvNZNpG`5&@G2D^0JwJjB#5CAVJYEAr5Qh;ug&V}|XH~8X07Z48tGbmWy3GYrdy3{>E>)Ga~ku@soW_gdX zxAV4Je#>{QbA*3C03Tu|=vWu)5?#hETf6A|h8=S6`kJhOyOML;joh_{@rDVW0ygC& zPFUJF?6}Xh&kC_STrb=-Z})2Z6~Z-{VF-#3PxCE$g7>fNX|3kf=Hn8cVEEkey*k!y ziSo6+)u)5T%-IjmpK~_=1@jNpc3q{Fs&d`$b<=ZNCr2{@V!CVZkT8nN(pie{<<+WB zU%V1GAJIyPRu$VDp`($!L|f-qx|bU~R1z0>-g^8SU*S`np^ipXbnM(YVA?O*P#zE4 zXLrlstXFs%Qk!i~K-_w#M$~LeOUvW7vHM7Z6OTGb_gLUvG0K+0ApCNVfQH8ov*zpt$$T&2t+UQ==4 z9xA$Hk~c4oS?&7aSLN5W4a3Tw7(6<+eHZDH$dN~kfSKEj`(PDSG7qZ2d)tn^6PQ1{ znZH_Rem4f341>*d-2PlN`&^wBpI$k*s{)x(|Yo9iZ zU%tURK|V+N6b^gB#awOZ*juH*uFnnGaw6r@35}hlam+ZJD?l;oR_M$^kl88)uHJ`= z)rkPBu0@gdyOcr)#KxOk`z$l)qvr>4C0P}R#yaaed0##IB@VoF=@NA2ib(UqYnR7L zJNxc`w9&x7B=KayvIKCFla)tw1`IavXlzinwE3iXzOsBdaZcvd6}z>bfsrydd&#=B z9Twb``wntiU@6D|M|gcg9NYLTvTaw(*Lz>ziSjR+^XZv%3iFH$EO%AOY5nCH*>iz6 z!o@1X{LaM3ZynRHPb!pUt5&N#T3)A;C;FU^Z?yPRai#re{WHO*!!IQPW}b6Cb3;GI zBu#no$X>tv@=I8RS%t6uqT>4McIHILI@glZH;UjTw9`*auWg;lUD;Y%|+vv6+YxBJB zOMHF{Ik7b9s8RSq$(M<} zYc6*@Q1$7KwNJwt`w!j&cXmJhib)-ULNsZ;GrF=Us4l|zKM!k3$erF_+4|v z-vKkmH{}27%~At{Wk+O9apYCPW&G{9AEtI?DZhB`o}cyRt#n57$iz=?L)$ktACg_u zY0O8e-wHC#@6I8#Z=iYkw2e<=uKD+CkvQe+#Hc)T#WnwV2~t3fqRC!yix81O+o2D> z3t@blMGsqM9dV0qanrRwm2<9@DW47YP`?%*Y$7R!qX26%2(YB+{=Vwo{qk!1 zSI+*rZ7doupTOGnnkHblsVb=1mSNPYyL_B*ar??M{+Tj^*4$(Hzn&elUB2Cy_($D2 z^o1PyU+h|~*3yX^9p+c;J!5Y%;7d16=485%0aGt6B(cO$Y?UILCAz+lE@$c8>|R&V ze~~a)el+r=!TFb&Zeo%TG6(Brjawo4vDrm+*)%ioB~E(j&4E*f&3}+MYoo_gOjvv4 zc^h8|eg?5prsSf!sk7@+DxQOb>v||-n#Tr)77!^ctm&6$>ryY{jRbP#r}H)b1*hd6 z-7eP|Ra4ubSDHMdyr$+G^{q?wIXap<=oew6R5?7bFZ67M)x4+e1Z_LI=o79~-VXlL zW9XxoFKtp!PlP8|<#dSlSWsIvn4Id{auj#9xw^XZiU-}avwYVYMfC9WQ5_GC_C>Y( zwDT%{U7I?s%<|qvcSm=mpasf0P-L~xCAS+L^Gb-H+WB=^(W!pF*f*=hrS&*;wU77X z_ba|lOp3hX;5X-6*kOPFVMlj_MdT9TJ6Ub>H%o6Y7FAJP8zU;(%Ls=CItac*bD?0} zP@~G%?(w2AOw!6(u=J1hqTi?%v5JEN+D!1csD9$XC+KMAL6Bn?R2}^UQ?X9RGQ?&^ z!Qe*YYSz!#<(o5V%8;z-Z9WTiV}&+pU)Xd?WUlfvHYOjtIHJ+xgFs6s1f3d6Lh*I^ z#f9poI&O&$MyCm|jGse+H`%y#=aZ6OaM*TBn^x-_KmK@%#|vo(5)H1Kn5Rlh7+kLj zE6M?Jm*^A{j?LN$gVq1)qMFMqjsylaGO}k@6>qxn?bY9<*K9xa+HkghKgou5M&2Rw ze2YZ3|LK*^*8RPd44P|Z3 zz8!RybSaoyDP{90?zrE)H*2`@EMDT88tL;dO3PlEnGjl?+-M;1G@i_^DVWH4MOr4;(Y!1lPBoB9fR;tTyoWEkwNOy5bX$X&F`&Te0!%;7H{cxH za$+d+pvtPMLg-EkmuT_u-+J+L^=YBw$4G3zMT0|%J#@w0{@ObceQ0LjXMQF{1J&{43zmld+>U9} zrVSZ9INIO;S*dWV%y;q;sj~dAp+n7%P;Hv~ey7uhFA-Hw&hi?F7EhUy#+f(Eep+1( z@UmV3y05au-E2av(=apr#84IG=d*Qn8O(NW(pc$oh$lH&rf~G@<<@z(-|gK!eezpaXJ>ucP72D|l5YHzcBL`f;SPqAIIyKOBE(aSA}Px(dfPi>e$S%8Vr zcl3_5Mf{s*81{ z|5tg}{ts2&$8EK-TZyM^DtD<+Eu~13orGvyLxw2je#vblN@a)aBFT^vX%K}`7)GU9 zmoRQ&O1TydGU?)0B%b$SpV#y3-|);2yx4{6-t5uqO zmjAGK{$7oY=g<*CgPQn3F$sibVz*Yl-IW;Z)ZnJf<+g5Z>HS856<(%VD4{6eTULK( z3s(L)nww34mF+5LMz$@(!W%c+kXL?0g&Y;CxRLBY*Ty#>a{9%s%zpu7Tr3i<6{!EaRjHaVy{(gRl_o z8U}%hkidZSA-}2Es@OEZGyOhe5bK%r0?=>Zg|5+yU-WFTf5?U$KdOr5*_74KKa?^o zfAnW!=C@0IlI&sct1s_UbEchHiWawOTB)z1DO)NGWh5VSpi?3+bBN((D+w&(Q}V{? z$;zYDt%PMVXvE3IrEXSm6$xh^4cXr8%y&3>IG(^A!Nfiq3k;C0BwYRQC@V@zNipj& zc~QQHiD^GJ2qc3IO$!N~U$BbDo$y~Ot=*ZS4Yxv^Gr*JFyIy9B%CTk^SRWG`YBfCfA6dWL_=+UEDE7g{YGd@#MklWV1DR`TV*0 z$JVDttB|`30+v$>ZByZNt91eNFODx)>OJp5&ohL9s7v09Qz%4AKkq3hW-_MpX{kfU zh`MoP*8l|KkX4yx6CDuh7kLwDc;X3>nkXF!reU;Zeey+r+7>!(^;mBDh}+R%2O8-{fKB zl0#wLp$}jJAg8w0tPwbJGd#fbFZtvc{$xy6}Plq5X;MT~8qur%ArFdTNN%@4TL!y^*&Gh%A@`gn8g?Gib{c*)y@s^@^S z*rA*S%g=*C$Q~;}@lxA1-2a_iH^D8C=B+pa>LHI+2dc^gT2u>T>zzCDB%1^Cn-eWA z)$V7Z(87*<8RNs8AB{Hd8d1y!0L|$$%5QM(0dH`Exxgdmv}`akd8)Kv1G};nx^ty0 zkIA{R9)3UbR4i^+n~0+8;V0~F1}i`!iVRXl9GVvYeiLzPL`{Q-^{w@5Az7H<2$6n#7=TyK5AI6V z%ucj*$mqZzPxGB+*zP&fvzM_{DxGvi0tjKD&oYsoEc-%a>6VQr(yh8X6dgG=XaB#%oo1S1J}0=I0c4obD0q%5hhT(33+DP z*2BZpa_UrgZp3Yx2#O1#_7_ai7Xddqxm-F@WoY7c6fL=rysr;;r8@D zd{PddQfsNY%6v05{#O9I(O`wpq6JVy{$$gGu=uo1tAUYg7C-foP(LHFIf5V1?(A7X zUHm{M(}JoY;XeD$M+2)dAuC5EzFp~-L}Z#qd`y*VubO9N1V48v??nX_egBZ{I=F?; zXx}K%GR`_RHLo)|8>=H@YH69-eM8)sLTQJZY~lHH-0873YnbuoASFoOJdS6#w3wH4 z><*~7cI{f)c4AXiS*6)QdHmW#Q(yG9#cdi!n|5%yEBz_}&1GLXMuMiK@@rb%3NzQU zyS%@bL|Ix|Dwf17FRS0M`QW!V`JtbM+V&F(R(ajN(&nL|GaytL4Uw(ymIj~JSH%hk zJQF@(lSzLFFWU2>4;Jglq&22(KM12dnM}5}zB1*RIMI=-M*AAh&wXn^5zpJyzp*!z zYIWC7CL`Q%mzrHx{hc$&BlLL{%MME{bt7jgKI{5XQ{uFgyr#A6f(S57QIF$@l$aM8IXf20#dp5-GF; z$`b&rboI1s2Sl?Gp+=wt`0M(xs131XMagVqmyRU6Puo+{BOwSXL6wP(Jli2TDM1r_sf^rP%pRI*xNHVGqHKj!8yW;nT=Jtt4kP> zL4$d3)z|gOs=6t)4ORK_jJxrmuUxgtL*2y4r~_qU4ga!PW<%n&XFWYiU7f`*6gQ#IOWJH| zU)S({>$4-DJuIxosGMJjb!Z6{JS!*ium&0cOTzcxsW zX1SefVQMPE?6Y(zz5@GN%I@a@Z11Gejp^@t4{$$eg!38h3ovK_OqJ=bWeN?r=Ah!r z=v(dB#uTY)vV=b0_m+>>w&FLMzw`1z&C?<7K?aj9Dx2bU7zBZH>mLuMKB`P~q?@Hi z#Ju!LDep9uiuXyOo1a;B6qyXDVYobB&4wk_vYCX4USLKK4;+9k;D>hVSgobH zd?=XU)*1^g12^O5=BBq}a^`yoMBC70Ksbf@Inmh@Qhv5Kn%Y`gU2I_~UzC$1fjK6qagj3XlfBjTeBm7qcLfVu(tAxw#kJg6Eb-b6*$>+fGE!oUuc zn|r`!@NdeC}V`r+j=&Ls*)w{r(AoEHE2*qSO3pYHOQ~J2%iSnQNvP^9pBv zi#{nOw*IL_-*&sl+JqSp?pIPIV%^7gN9mb6F@6dvv&R=O&bQ_*(izUh#NazxbSq0s zF*TbU4o71PW9LP8!LPty7nOY_^C4z|FhF#6ST$^BZGl&WM|1y#>)mNvTU&Ug^f67{ z+?=r`;6WGsLsO#zCV(v1&SEVptJ2F~yCo&WDLc9^JpTmkF}(?6Yh@lp&Q81WywKks+JD5oOK(EG4`5==eNw&2XvE(J$#bcf zR#l%5%FfH9KIkGvqF%d@Yy-D1nM$RSA~_txl*uW_ixDcM=`(_k>#ReL&{Y1#T=?m6 z=qCR`#YuSiO@Z? zH@pgv8L8T5){OPw>%k#^`yMJw&@h1MFYG#_BVjuHgMbK0S2bB{{)2j52tg&Pmz*^`Z?~llj~x1pq4D5wLXb)*n|9sP@tk{)xylmnp(k{qf=HP~1+TVa z2gZgYMj01iY?ryG~NL z8YMV9-mmOMTrnc@hl2gOza4CDR)qj!t%Xpf#B=;*>bx^XrwOXOkdTmY5zIRX%=&Tm zSNP2F%An8bx4R$?gjN-Yykq?z#AU7$bX;VG%M3cc0@Vtv=iVnt0+n%l4c$(znL8r< zp>qiPq3{LyJ$rgS&;%k8E&88-5jf`+bn?HUYDfMNs^&Fcg3SMJ@!yubxu0JCZxY-8 c!GD@rxKv>AKmq&xoFi`kK4V=b=}6eW0T``0qW}N^ literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/garage.drawio.pdf b/doc/talks/2022-06-23-stack/assets/garage.drawio.pdf new file mode 100644 index 00000000..2f7a5dcb --- /dev/null +++ b/doc/talks/2022-06-23-stack/assets/garage.drawio.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d9ca1a04f943664e14d0e1edbde596789e4798f8ad5e1cff68bc9dd0cc1334f +size 26098 diff --git a/doc/talks/2022-06-23-stack/assets/garage.drawio.png b/doc/talks/2022-06-23-stack/assets/garage.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..386dd862f3f9453d29dd4a37a1e49e5be82d1db4 GIT binary patch literal 13463 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 literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/garage2.drawio.png b/doc/talks/2022-06-23-stack/assets/garage2.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..8562fbcfa4ada2d4d45d0f29d1a1bba1c7e98887 GIT binary patch literal 89618 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

o_D@<%TmszKx6LvQ#p}XvsmA+xQN4;}b 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 literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/garage2a.drawio.pdf b/doc/talks/2022-06-23-stack/assets/garage2a.drawio.pdf new file mode 100644 index 00000000..c4f5e0b7 --- /dev/null +++ b/doc/talks/2022-06-23-stack/assets/garage2a.drawio.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bffa5d4a72ad25e0b18b43022258a2e4471ce94ff5397aab892fe43ec9d4d7d5 +size 33911 diff --git a/doc/talks/2022-06-23-stack/assets/garage2b.drawio.pdf b/doc/talks/2022-06-23-stack/assets/garage2b.drawio.pdf new file mode 100644 index 00000000..1a12a0d3 --- /dev/null +++ b/doc/talks/2022-06-23-stack/assets/garage2b.drawio.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c4913b52e84b5af3f8effbcb7e2060845a8c6f43d5b38072b9ee33c0300d49f +size 31051 diff --git a/doc/talks/2022-06-23-stack/assets/garage_tables.svg b/doc/talks/2022-06-23-stack/assets/garage_tables.svg new file mode 100644 index 00000000..c7172713 --- /dev/null +++ b/doc/talks/2022-06-23-stack/assets/garage_tables.svg @@ -0,0 +1,537 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + Object + + + + + + bucket + + + + + + file path + + = partition key + + = sort key + + + + + + Version 1 + deleted + + + + + + + Version 2 + id + + size + MIME type + ... + + + + + + Version + + id + h(block 1) + h(block 2) + ... + + + + + Data block + + hash + data + + + + Objects table + Versions table + Blocks table + + diff --git a/doc/talks/2022-06-23-stack/assets/garageuses.png b/doc/talks/2022-06-23-stack/assets/garageuses.png new file mode 100644 index 0000000000000000000000000000000000000000..b66d7f30352a237f6e419df134c39b80621bc8e8 GIT binary patch literal 53251 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 q|NZX&|0?DGa|i!xb#V^5 literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/inframap.jpg b/doc/talks/2022-06-23-stack/assets/inframap.jpg new file mode 100644 index 0000000000000000000000000000000000000000..19905a99d8c5cd2f84f458208d8f93cfbce3fc1c GIT binary patch literal 38247 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 literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/location-aware.png b/doc/talks/2022-06-23-stack/assets/location-aware.png new file mode 100644 index 0000000000000000000000000000000000000000..f5966865e4337fd98d6ff6d104fa47cc96fd16c8 GIT binary patch literal 99269 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 z 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
6h2!AMXWTPvDW*g>! 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�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

  • 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 literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/neptune.jpg b/doc/talks/2022-06-23-stack/assets/neptune.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e59f0bfa2bb76b977164425b3119028ee998b12e GIT binary patch literal 177936 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 z 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
      k|*`{m)S!ey+ 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

      qDeL$?M&n^1R{-L#5En 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 literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/quentin.jpg b/doc/talks/2022-06-23-stack/assets/quentin.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d9a7b1e7ceda06452e1013883d11bc2d63434ad4 GIT binary patch literal 39221 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 literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/rust_logo.png b/doc/talks/2022-06-23-stack/assets/rust_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0e4809ec3fb8275d20b8f1d3007a084b61711392 GIT binary patch literal 14835 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 literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/slide1.png b/doc/talks/2022-06-23-stack/assets/slide1.png new file mode 100644 index 0000000000000000000000000000000000000000..eb2e67a05635731973bea12d280f911e7db30d09 GIT binary patch literal 89059 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

      FCU>^_2}%C3}U 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 YXf?duU|xP$i@f?#O&yJ#Lzcn+2NHe8&Hw-a literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/slide2.png b/doc/talks/2022-06-23-stack/assets/slide2.png new file mode 100644 index 0000000000000000000000000000000000000000..126a39b82879d14b845605a00921d04f9f3dce6c GIT binary patch literal 83364 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

      jZ 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�Mb+E%{%^%|K}a~U1sK6dzdbDx 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

      s?kR=d==5?uA;RY{VF{>^52MPtb%>PpWUqE;s1 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 B&G-eVrH`K@1M@=A=bHyem)KITX>y8)Mnib@}<( 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 literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/slideB2.png b/doc/talks/2022-06-23-stack/assets/slideB2.png new file mode 100644 index 0000000000000000000000000000000000000000..a881a796a2fb83c0920718973b25f54b3d872c20 GIT binary patch literal 83399 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 literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/slideB3.png b/doc/talks/2022-06-23-stack/assets/slideB3.png new file mode 100644 index 0000000000000000000000000000000000000000..830709d22add0194f1362d049286bcabd06f58cc GIT binary patch literal 82581 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

      Zg*5JD<}6Grg84$$B)WYbVCK& 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≪)IID(I9n9ghPT`{j;v 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 literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/slides.svg b/doc/talks/2022-06-23-stack/assets/slides.svg new file mode 100644 index 00000000..9946c6fb --- /dev/null +++ b/doc/talks/2022-06-23-stack/assets/slides.svg @@ -0,0 +1,4326 @@ + + + + + + + + + + + + + + + + + + + + + + User-facing application + Database + Filesystem + + + + + + + + + + diff --git a/doc/talks/2022-06-23-stack/assets/slidesB.svg b/doc/talks/2022-06-23-stack/assets/slidesB.svg new file mode 100644 index 00000000..c0a6e97c --- /dev/null +++ b/doc/talks/2022-06-23-stack/assets/slidesB.svg @@ -0,0 +1,444 @@ + + + +User-facing applicationDatabase*K2VObject storage*(not really a database)Database diff --git a/doc/talks/2022-06-23-stack/talk.pdf b/doc/talks/2022-06-23-stack/talk.pdf new file mode 100644 index 00000000..880f83d6 --- /dev/null +++ b/doc/talks/2022-06-23-stack/talk.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f245bb017eb85a227d96e576ae821f165ef478d46de02535c196638aa5fc84b0 +size 2575953 diff --git a/doc/talks/2022-06-23-stack/talk.tex b/doc/talks/2022-06-23-stack/talk.tex new file mode 100644 index 00000000..206af43a --- /dev/null +++ b/doc/talks/2022-06-23-stack/talk.tex @@ -0,0 +1,480 @@ +%\nonstopmode +\documentclass[aspectratio=169]{beamer} +\usepackage[utf8]{inputenc} +% \usepackage[frenchb]{babel} +\usepackage{amsmath} +\usepackage{mathtools} +\usepackage{breqn} +\usepackage{multirow} +\usetheme{boxes} +\usepackage{graphicx} +\usepackage{adjustbox} +%\useoutertheme[footline=authortitle,subsection=false]{miniframes} +%\useoutertheme[footline=authorinstitute,subsection=false]{miniframes} +\useoutertheme{infolines} +\setbeamertemplate{headline}{} + +\beamertemplatenavigationsymbolsempty + +\definecolor{TitleOrange}{RGB}{255,137,0} +\setbeamercolor{title}{fg=TitleOrange} +\setbeamercolor{frametitle}{fg=TitleOrange} + +\definecolor{ListOrange}{RGB}{255,145,5} +\setbeamertemplate{itemize item}{\color{ListOrange}$\blacktriangleright$} + +\definecolor{verygrey}{RGB}{70,70,70} +\setbeamercolor{normal text}{fg=verygrey} + + +\usepackage{tabu} +\usepackage{multicol} +\usepackage{vwcol} +\usepackage{stmaryrd} +\usepackage{graphicx} + +\usepackage[normalem]{ulem} + +\title{Introducing Garage} +\subtitle{a new storage platform for self-hosted geo-distributed clusters} +\author{Deuxfleurs Association} +\date{IMT Atlantique, 2022-06-23} + +\begin{document} + +\begin{frame} + \centering + \includegraphics[width=.3\linewidth]{../../sticker/Garage.pdf} + \vspace{1em} + + {\large\bf Deuxfleurs Association} + \vspace{1em} + + \url{https://garagehq.deuxfleurs.fr/} + + Matrix channel: \texttt{\#garage:deuxfleurs.fr} +\end{frame} + +\begin{frame} + \frametitle{Who we are} + \begin{columns}[t] + \begin{column}{.2\textwidth} + \centering + \adjincludegraphics[width=.4\linewidth, valign=t]{assets/alex.jpg} + \end{column} + \begin{column}{.6\textwidth} + \textbf{Alex Auvolat}\\ + PhD at Inria, team WIDE; co-founder of Deuxfleurs + \end{column} + \begin{column}{.2\textwidth} + ~ + \end{column} + \end{columns} + \vspace{1em} + + \begin{columns}[t] + \begin{column}{.2\textwidth} + ~ + \end{column} + \begin{column}{.6\textwidth} + \textbf{Quentin Dufour}\\ + PhD at Inria, team WIDE; co-founder of Deuxfleurs + \end{column} + \begin{column}{.2\textwidth} + \centering + \adjincludegraphics[width=.5\linewidth, valign=t]{assets/quentin.jpg} + \end{column} + \end{columns} + \vspace{2em} + + \begin{columns}[t] + \begin{column}{.2\textwidth} + \centering + \adjincludegraphics[width=.5\linewidth, valign=t]{assets/deuxfleurs.pdf} + \end{column} + \begin{column}{.6\textwidth} + \textbf{Deuxfleurs}\\ + A non-profit self-hosting collective,\\ + member of the CHATONS network + \end{column} + \begin{column}{.2\textwidth} + \centering + \adjincludegraphics[width=.7\linewidth, valign=t]{assets/logo_chatons.png} + \end{column} + \end{columns} + +\end{frame} + +\begin{frame} + \frametitle{Our objective at Deuxfleurs} + + \begin{center} + \textbf{Promote self-hosting and small-scale hosting\\ + as an alternative to large cloud providers} + \end{center} + \vspace{2em} + \visible<2->{ + Why is it hard? + } + \visible<3->{ + \vspace{2em} + \begin{center} + \textbf{\underline{Resilience}}\\ + {\footnotesize (we want good uptime/availability with low supervision)} + \end{center} + } +\end{frame} + +\begin{frame} + \frametitle{How to make a \underline{stable} system} + + Enterprise-grade systems typically employ: + \vspace{1em} + \begin{itemize} + \item RAID + \item Redundant power grid + UPS + \item Redundant Internet connections + \item Low-latency links + \item ... + \end{itemize} + \vspace{1em} + $\to$ it's costly and only worth it at DC scale +\end{frame} + +\begin{frame} + \frametitle{How to make a \underline{resilient} system} + + \only<1,4-5>{ + Instead, we use: + \vspace{1em} + \begin{itemize} + \item \textcolor<2->{gray}{Commodity hardware (e.g. old desktop PCs)} + \vspace{.5em} + \item<4-> \textcolor<5->{gray}{Commodity Internet (e.g. FTTB, FTTH) and power grid} + \vspace{.5em} + \item<5-> \textcolor<6->{gray}{\textbf{Geographical redundancy} (multi-site replication)} + \end{itemize} + } + \only<2>{ + \begin{center} + \includegraphics[width=.8\linewidth]{assets/atuin.jpg} + \end{center} + } + \only<3>{ + \begin{center} + \includegraphics[width=.8\linewidth]{assets/neptune.jpg} + \end{center} + } + \only<6>{ + \begin{center} + \includegraphics[width=.5\linewidth]{assets/inframap.jpg} + \end{center} + } +\end{frame} + +\begin{frame} + \frametitle{How to make this happen} + \begin{center} + \only<1>{\includegraphics[width=.8\linewidth]{assets/slide1.png}}% + \only<2>{\includegraphics[width=.8\linewidth]{assets/slide2.png}}% + \only<3>{\includegraphics[width=.8\linewidth]{assets/slide3.png}}% + \end{center} +\end{frame} + +\begin{frame} + \frametitle{Distributed file systems are slow} + File systems are complex, for example: + \vspace{1em} + \begin{itemize} + \item Concurrent modification by several processes + \vspace{1em} + \item Folder hierarchies + \vspace{1em} + \item Other requirements of the POSIX spec + \end{itemize} + \vspace{1em} + Coordination in a distributed system is costly + + \vspace{1em} + Costs explode with commodity hardware / Internet connections\\ + {\small (we experienced this!)} +\end{frame} + +\begin{frame} + \frametitle{A simpler solution: object storage} + Only two operations: + \vspace{1em} + \begin{itemize} + \item Put an object at a key + \vspace{1em} + \item Retrieve an object from its key + \end{itemize} + \vspace{1em} + {\footnotesize (and a few others)} + + \vspace{1em} + Sufficient for many applications! +\end{frame} + +\begin{frame} + \frametitle{A simpler solution: object storage} + \begin{center} + \includegraphics[width=.2\linewidth]{../2020-12-02_wide-team/img/Amazon-S3.jpg} + \hspace{5em} + \includegraphics[width=.2\linewidth]{assets/minio.png} + \end{center} + \vspace{1em} + S3: a de-facto standard, many compatible applications + + \vspace{1em} + + MinIO is self-hostable but not suited for geo-distributed deployments +\end{frame} + + +\begin{frame} + \frametitle{But what is Garage, exactly?} + \textbf{Garage is a self-hosted drop-in replacement for the Amazon S3 object store}\\ + \vspace{.5em} + that implements resilience through geographical redundancy on commodity hardware + \begin{center} + \includegraphics[width=.8\linewidth]{assets/garageuses.png} + \end{center} +\end{frame} + +\begin{frame} + \frametitle{Overview} + \begin{center} + \only<1>{\includegraphics[width=.45\linewidth]{assets/garage2a.drawio.pdf}}% + \only<2>{\includegraphics[width=.45\linewidth]{assets/garage2b.drawio.pdf}}% + \end{center} +\end{frame} + +\begin{frame} + \frametitle{Garage is \emph{location-aware}} + \begin{center} + \includegraphics[width=\linewidth]{assets/location-aware.png} + \end{center} + \vspace{2em} + Garage replicates data on different zones when possible +\end{frame} + +\begin{frame} + \frametitle{Garage is \emph{location-aware}} + \begin{center} + \includegraphics[width=.8\linewidth]{assets/map.png} + \end{center} +\end{frame} + +\begin{frame} + \frametitle{How to spread files over different cluster nodes?} + \textbf{Consistent hashing (DynamoDB):} + \vspace{1em} + + \begin{center} + \only<1>{\includegraphics[width=.45\columnwidth]{assets/consistent_hashing_1.pdf}}% + \only<2>{\includegraphics[width=.45\columnwidth]{assets/consistent_hashing_2.pdf}}% + \only<3>{\includegraphics[width=.45\columnwidth]{assets/consistent_hashing_3.pdf}}% + \only<4>{\includegraphics[width=.45\columnwidth]{assets/consistent_hashing_4.pdf}}% + \end{center} +\end{frame} + +\begin{frame} + \frametitle{How to spread files over different cluster nodes?} + \textbf{Issues with consistent hashing:} + \vspace{1em} + \begin{itemize} + \item Doesn't dispatch data based on geographical location of nodes + \vspace{1em} + \item<2-> Geographically aware adaptation, try 1:\\ + data quantities not well balanced between nodes + \vspace{1em} + \item<3-> Geographically aware adaptation, try 2:\\ + too many reshuffles when adding/removing nodes + \end{itemize} +\end{frame} + +\begin{frame} + \frametitle{How to spread files over different cluster nodes?} + \textbf{Garage's method: build an index table} + \vspace{1em} + + Realization: we can actually precompute an optimal solution + \vspace{1em} + + \visible<2->{ + \begin{center} + \begin{tabular}{|l|l|l|l|} + \hline + \textbf{Partition} & \textbf{Node 1} & \textbf{Node 2} & \textbf{Node 3} \\ + \hline + \hline + Partition 0 & Io (jupiter) & Drosera (atuin) & Courgette (neptune) \\ + \hline + Partition 1 & Datura (atuin) & Courgette (neptune) & Io (jupiter) \\ + \hline + Partition 2 & Io(jupiter) & Celeri (neptune) & Drosera (atuin) \\ + \hline + \hspace{1em}$\vdots$ & \hspace{1em}$\vdots$ & \hspace{1em}$\vdots$ & \hspace{1em}$\vdots$ \\ + \hline + Partition 255 & Concombre (neptune) & Io (jupiter) & Drosera (atuin) \\ + \hline + \end{tabular} + \end{center} + } + \vspace{1em} + \visible<3->{ + The index table is built centrally using an optimal* algorithm,\\ + then propagated to all nodes\\ + \hfill\footnotesize *not yet optimal but will be soon + } +\end{frame} + +\begin{frame} + \frametitle{Garage's internal data structures} + \centering + \includegraphics[width=.75\columnwidth]{assets/garage_tables.pdf} +\end{frame} + +%\begin{frame} +% \frametitle{Garage's architecture} +% \begin{center} +% \includegraphics[width=.35\linewidth]{assets/garage.drawio.pdf} +% \end{center} +%\end{frame} + +\begin{frame} + \frametitle{Garage is \emph{coordination-free}:} + \begin{itemize} + \item No Raft or Paxos + \vspace{1em} + \item Internal data types are CRDTs + \vspace{1em} + \item All nodes are equivalent (no master/leader/index node) + \end{itemize} + \vspace{2em} + $\to$ less sensitive to higher latencies between nodes +\end{frame} + +\begin{frame} + \frametitle{Consistency model} + \begin{itemize} + \item Not ACID (not required by S3 spec) / not linearizable + \vspace{1em} + \item \textbf{Read-after-write consistency}\\ + {\footnotesize (stronger than eventual consistency)} + \end{itemize} +\end{frame} + +\begin{frame} + \frametitle{Impact on performances} + \begin{center} + \includegraphics[width=.8\linewidth]{assets/endpoint-latency-dc.png} + \end{center} +\end{frame} + + +\begin{frame} + \frametitle{An ever-increasing compatibility list} + \begin{center} + \includegraphics[width=.7\linewidth]{assets/compatibility.png} + \end{center} +\end{frame} + +\begin{frame} + \frametitle{Further plans for Garage} + \begin{center} + \only<1>{\includegraphics[width=.8\linewidth]{assets/slideB1.png}}% + \only<2>{\includegraphics[width=.8\linewidth]{assets/slideB2.png}}% + \only<3>{\includegraphics[width=.8\linewidth]{assets/slideB3.png}}% + \end{center} +\end{frame} + +\begin{frame} + \frametitle{K2V Design} + \begin{itemize} + \item A new, custom, minimal API + \vspace{1em} + \item<2-> Exposes the partitoning mechanism of Garage\\ + K2V = partition key / sort key / value (like Dynamo) + \vspace{1em} + \item<3-> Coordination-free, CRDT-friendly (inspired by Riak)\\ + \vspace{1em} + \item<4-> Cryptography-friendly: values are binary blobs + \end{itemize} +\end{frame} + +\begin{frame} + \frametitle{Application: an e-mail storage server} + \begin{center} + \only<1>{\includegraphics[width=.9\linewidth]{assets/aerogramme.png}}% + \end{center} +\end{frame} + +\begin{frame} + \frametitle{Aerogramme data model} + \begin{center} + \only<1>{\includegraphics[width=.4\linewidth]{assets/aerogramme_datatype.drawio.pdf}}% + \only<2->{\includegraphics[width=.9\linewidth]{assets/aerogramme_keys.drawio.pdf}\vspace{1em}}% + \end{center} + \visible<3->{Aerogramme encrypts all stored values for privacy\\ + (Garage server administrators can't read your mail)} +\end{frame} + +\begin{frame} + \frametitle{Different deployment scenarios} + \begin{center} + \only<1>{\includegraphics[width=.9\linewidth]{assets/aerogramme_components1.drawio.pdf}}% + \only<2>{\includegraphics[width=.9\linewidth]{assets/aerogramme_components2.drawio.pdf}}% + \end{center} +\end{frame} + +\begin{frame} + \frametitle{A new model for building resilient software} + \begin{itemize} + \item Design a data model suited to K2V\\ + {\footnotesize (see Cassandra docs on porting SQL data models to Cassandra)} + \vspace{1em} + \begin{itemize} + \item Use CRDTs or other eventually consistent data types (see e.g. Bayou) + \vspace{1em} + \item Store opaque binary blobs to provide End-to-End Encryption\\ + \end{itemize} + \vspace{1em} + \item Store big blobs (files) in S3 + \vspace{1em} + \item Let Garage manage sharding, replication, failover, etc. + \end{itemize} +\end{frame} + +\begin{frame} + \frametitle{Research perspectives} + \begin{itemize} + \item Write about Garage's global architecture \emph{(paper in progress)} + \vspace{1em} + \item Measure and improve Garage's performances + \vspace{1em} + \item Discuss the optimal layout algorithm, provide proofs + \vspace{1em} + \item Write about our proposed architecture for (E2EE) apps over K2V+S3 + \end{itemize} +\end{frame} + +\begin{frame} + \frametitle{Where to find us} + \begin{center} + \includegraphics[width=.25\linewidth]{../../logo/garage_hires.png}\\ + \vspace{-1em} + \url{https://garagehq.deuxfleurs.fr/}\\ + \url{mailto:garagehq@deuxfleurs.fr}\\ + \texttt{\#garage:deuxfleurs.fr} on Matrix + + \vspace{1.5em} + \includegraphics[width=.06\linewidth]{assets/rust_logo.png} + \includegraphics[width=.13\linewidth]{assets/AGPLv3_Logo.png} + \end{center} +\end{frame} + +\end{document} + +%% vim: set ts=4 sw=4 tw=0 noet spelllang=en : From b74b533b7be4f43bb565a5f025cf6121933307f6 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 29 Jun 2022 11:50:51 +0200 Subject: [PATCH 026/149] Fix typo --- doc/book/reference-manual/s3-compatibility.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/book/reference-manual/s3-compatibility.md b/doc/book/reference-manual/s3-compatibility.md index a8e503d5..3d571264 100644 --- a/doc/book/reference-manual/s3-compatibility.md +++ b/doc/book/reference-manual/s3-compatibility.md @@ -148,7 +148,7 @@ Please open an issue if you have a use case for replication. | [PutBucketReplication](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketReplication.html) | ❌ Missing | ❌| ⚠ | ❌| ❌| *Note: Ceph documentation briefly says that Ceph supports -[replication though the S3 API](https://docs.ceph.com/en/latest/radosgw/multisite-sync-policy/#s3-replication-api) +[replication through the S3 API](https://docs.ceph.com/en/latest/radosgw/multisite-sync-policy/#s3-replication-api) but with some limitations. Additionaly, replication endpoints are not documented in the S3 compatibility page so I don't know what kind of support we can expect.* From 0850bac874029f0b8b278d75537dd037e5db57da Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 4 Jul 2022 12:45:32 +0200 Subject: [PATCH 027/149] Add `poll` command to `k2v-cli` (#335) Co-authored-by: Alex Auvolat Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/335 Co-authored-by: Alex Co-committed-by: Alex --- src/k2v-client/bin/k2v-cli.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/k2v-client/bin/k2v-cli.rs b/src/k2v-client/bin/k2v-cli.rs index 38c39361..884e7438 100644 --- a/src/k2v-client/bin/k2v-cli.rs +++ b/src/k2v-client/bin/k2v-cli.rs @@ -55,6 +55,19 @@ enum Command { #[clap(flatten)] output_kind: ReadOutputKind, }, + /// Watch changes on a single value + Poll { + /// Partition key to delete from + partition_key: String, + /// Sort key to delete from + sort_key: String, + /// Causality information + #[clap(short, long)] + causality: String, + /// Output formating + #[clap(flatten)] + output_kind: ReadOutputKind, + }, /// Delete a single value Delete { /// Partition key to delete from @@ -324,6 +337,21 @@ async fn main() -> Result<(), Error> { let res = client.read_item(&partition_key, &sort_key).await?; output_kind.display_output(res); } + Command::Poll { + partition_key, + sort_key, + causality, + output_kind, + } => { + let res_opt = client + .poll_item(&partition_key, &sort_key, causality.into(), None) + .await?; + if let Some(res) = res_opt { + output_kind.display_output(res); + } else { + println!("Delay expired and value didn't change."); + } + } Command::ReadIndex { output_kind, filter, From b6d59ec19a3d41ce581716cf0dda5d47c2785843 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 4 Jul 2022 14:00:02 +0200 Subject: [PATCH 028/149] Fix poll item when item didn't change --- src/k2v-client/bin/k2v-cli.rs | 9 ++++++++- src/k2v-client/lib.rs | 8 ++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/k2v-client/bin/k2v-cli.rs b/src/k2v-client/bin/k2v-cli.rs index 884e7438..925ebeb8 100644 --- a/src/k2v-client/bin/k2v-cli.rs +++ b/src/k2v-client/bin/k2v-cli.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use k2v_client::*; use garage_util::formater::format_table; @@ -64,6 +66,9 @@ enum Command { /// Causality information #[clap(short, long)] causality: String, + /// Timeout, in seconds + #[clap(short, long)] + timeout: Option, /// Output formating #[clap(flatten)] output_kind: ReadOutputKind, @@ -341,10 +346,12 @@ async fn main() -> Result<(), Error> { partition_key, sort_key, causality, + timeout, output_kind, } => { + let timeout = timeout.map(Duration::from_secs); let res_opt = client - .poll_item(&partition_key, &sort_key, causality.into(), None) + .poll_item(&partition_key, &sort_key, causality.into(), timeout) .await?; if let Some(res) = res_opt { output_kind.display_output(res); diff --git a/src/k2v-client/lib.rs b/src/k2v-client/lib.rs index 95974d7a..c2606af4 100644 --- a/src/k2v-client/lib.rs +++ b/src/k2v-client/lib.rs @@ -122,14 +122,14 @@ impl K2vClient { let res = self.dispatch(req, Some(timeout + DEFAULT_TIMEOUT)).await?; - let causality = res - .causality_token - .ok_or_else(|| Error::InvalidResponse("missing causality token".into()))?; - if res.status == StatusCode::NOT_MODIFIED { return Ok(None); } + let causality = res + .causality_token + .ok_or_else(|| Error::InvalidResponse("missing causality token".into()))?; + if res.status == StatusCode::NO_CONTENT { return Ok(Some(CausalValue { causality, From fe3fa83de74b79ffeeb2042c58b9360defa65431 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 4 Jul 2022 18:27:25 +0200 Subject: [PATCH 029/149] Publish k2v-client crate to crates.io (#337) Co-authored-by: Alex Auvolat Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/337 Co-authored-by: Alex Co-committed-by: Alex --- src/k2v-client/Cargo.toml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/k2v-client/Cargo.toml b/src/k2v-client/Cargo.toml index 224414ab..2f8a2679 100644 --- a/src/k2v-client/Cargo.toml +++ b/src/k2v-client/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "k2v-client" -version = "0.1.0" +version = "0.0.1" +authors = ["Trinity Pointard ", "Alex Auvolat "] edition = "2018" +license = "AGPL-3.0" +description = "Client library for the Garage K2V protocol" +repository = "https://git.deuxfleurs.fr/Deuxfleurs/garage" +readme = "../../README.md" [dependencies] base64 = "0.13.0" @@ -17,7 +22,7 @@ tokio = "1.17.0" # cli deps clap = { version = "3.1.18", optional = true, features = ["derive", "env"] } -garage_util = { path = "../util", optional = true } +garage_util = { version = "0.7.0", path = "../util", optional = true } [features] From aab34bfe5415e9584432bf32e29a151dc5af9ebd Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 4 Jul 2022 12:53:47 +0200 Subject: [PATCH 030/149] add delays in k2v test_items_and_indices --- src/garage/tests/k2v/item.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/garage/tests/k2v/item.rs b/src/garage/tests/k2v/item.rs index bf2b01f8..32537336 100644 --- a/src/garage/tests/k2v/item.rs +++ b/src/garage/tests/k2v/item.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use crate::common; use assert_json_diff::assert_json_eq; @@ -86,6 +88,7 @@ async fn test_items_and_indices() { assert_eq!(res_body, content); // ReadIndex -- now there should be some stuff + tokio::time::sleep(Duration::from_secs(1)).await; let res = ctx .k2v .request @@ -154,6 +157,7 @@ async fn test_items_and_indices() { assert_eq!(res_body, content2); // ReadIndex -- now there should be some stuff + tokio::time::sleep(Duration::from_secs(1)).await; let res = ctx .k2v .request @@ -222,6 +226,7 @@ async fn test_items_and_indices() { ); // ReadIndex -- now there should be some stuff + tokio::time::sleep(Duration::from_secs(1)).await; let res = ctx .k2v .request @@ -290,6 +295,7 @@ async fn test_items_and_indices() { assert_eq!(res.status(), 204); // ReadIndex -- now there should be some stuff + tokio::time::sleep(Duration::from_secs(1)).await; let res = ctx .k2v .request From 4f38cadf6e2963a652ed28327d1c2ccfa2ebb2b7 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Jul 2022 13:30:26 +0200 Subject: [PATCH 031/149] Background task manager (#332) - [x] New background worker trait - [x] Adapt all current workers to use new API - [x] Command to list currently running workers, and whether they are active, idle, or dead - [x] Error reporting - Optimizations - [x] Merkle updater: several items per iteration - [ ] Use `tokio::task::spawn_blocking` where appropriate so that CPU-intensive tasks don't block other things going on - scrub: - [x] have only one worker with a channel to start/pause/cancel - [x] automatic scrub - [x] ability to view and change tranquility from CLI - [x] persistence of a few info - [ ] Testing Co-authored-by: Alex Auvolat Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/332 Co-authored-by: Alex Co-committed-by: Alex --- Cargo.lock | 195 ++++++++++++- Cargo.nix | 254 ++++++++++++++++- Makefile | 22 +- src/block/Cargo.toml | 1 + src/block/lib.rs | 1 + src/block/manager.rs | 329 ++++++++-------------- src/block/repair.rs | 444 ++++++++++++++++++++++++++++++ src/db/Cargo.toml | 2 +- src/db/sqlite_adapter.rs | 2 +- src/garage/Cargo.toml | 2 + src/garage/admin.rs | 29 +- src/garage/cli/cmd.rs | 8 +- src/garage/cli/structs.rs | 55 +++- src/garage/cli/util.rs | 58 ++++ src/garage/repair/online.rs | 222 +++++++++------ src/model/index_counter.rs | 169 +++++++----- src/rpc/system.rs | 6 +- src/table/gc.rs | 89 ++++-- src/table/merkle.rs | 87 +++--- src/table/sync.rs | 198 +++++++------ src/util/Cargo.toml | 1 + src/util/background.rs | 160 ----------- src/util/background/job_worker.rs | 48 ++++ src/util/background/mod.rs | 117 ++++++++ src/util/background/worker.rs | 261 ++++++++++++++++++ src/util/lib.rs | 1 - src/util/tranquilizer.rs | 27 +- 27 files changed, 2050 insertions(+), 738 deletions(-) create mode 100644 src/block/repair.rs delete mode 100644 src/util/background.rs create mode 100644 src/util/background/job_worker.rs create mode 100644 src/util/background/mod.rs create mode 100644 src/util/background/worker.rs diff --git a/Cargo.lock b/Cargo.lock index ecdf8a57..e1ccfc2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,6 +93,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "autocfg" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.1.0", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -543,7 +552,7 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" dependencies = [ - "autocfg", + "autocfg 1.1.0", "cfg-if 1.0.0", "crossbeam-utils 0.8.8", "lazy_static", @@ -959,6 +968,7 @@ dependencies = [ "futures", "futures-util", "garage_api", + "garage_block", "garage_db", "garage_model 0.7.0", "garage_rpc 0.7.0", @@ -984,6 +994,7 @@ dependencies = [ "sha2", "static_init", "structopt", + "timeago", "tokio", "toml", "tracing", @@ -1038,6 +1049,7 @@ dependencies = [ name = "garage_block" version = "0.7.0" dependencies = [ + "arc-swap", "async-trait", "bytes 1.1.0", "futures", @@ -1065,11 +1077,11 @@ dependencies = [ "err-derive 0.3.1", "heed", "hexdump", - "log", "mktemp", "pretty_env_logger", "rusqlite", "sled", + "tracing", ] [[package]] @@ -1258,6 +1270,7 @@ dependencies = [ name = "garage_util" version = "0.7.0" dependencies = [ + "async-trait", "blake2", "chrono", "err-derive 0.3.1", @@ -1629,7 +1642,7 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" dependencies = [ - "autocfg", + "autocfg 1.1.0", "hashbrown", ] @@ -1651,6 +1664,16 @@ dependencies = [ "serde", ] +[[package]] +name = "isolang" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "265ef164908329e47e753c769b14cbb27434abf0c41984dca201484022f09ce5" +dependencies = [ + "phf", + "phf_codegen", +] + [[package]] name = "itertools" version = "0.4.19" @@ -1714,7 +1737,7 @@ dependencies = [ [[package]] name = "k2v-client" -version = "0.1.0" +version = "0.0.1" dependencies = [ "base64", "clap 3.1.18", @@ -1975,7 +1998,7 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" dependencies = [ - "autocfg", + "autocfg 1.1.0", ] [[package]] @@ -2137,7 +2160,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" dependencies = [ - "autocfg", + "autocfg 1.1.0", "num-traits", ] @@ -2147,7 +2170,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ - "autocfg", + "autocfg 1.1.0", ] [[package]] @@ -2216,7 +2239,7 @@ version = "0.9.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" dependencies = [ - "autocfg", + "autocfg 1.1.0", "cc", "libc", "openssl-src", @@ -2386,6 +2409,44 @@ dependencies = [ "indexmap", ] +[[package]] +name = "phf" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" +dependencies = [ + "phf_shared", + "rand 0.6.5", +] + +[[package]] +name = "phf_shared" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "0.4.29" @@ -2640,6 +2701,25 @@ dependencies = [ "winapi", ] +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.8", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi", +] + [[package]] name = "rand" version = "0.8.5" @@ -2647,10 +2727,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", + "rand_chacha 0.3.1", "rand_core 0.6.3", ] +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.3.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -2685,6 +2775,77 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[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.11" @@ -3107,6 +3268,12 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" + [[package]] name = "slab" version = "0.4.5" @@ -3355,6 +3522,16 @@ dependencies = [ "num_threads", ] +[[package]] +name = "timeago" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ec32dde57efb15c035ac074118d7f32820451395f28cb0524a01d4e94983b26" +dependencies = [ + "chrono", + "isolang", +] + [[package]] name = "tinyvec" version = "1.5.1" diff --git a/Cargo.nix b/Cargo.nix index e5155e61..37bf3186 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -172,6 +172,16 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".autocfg."0.1.8" = overridableMkRustCrate (profileName: rec { + name = "autocfg"; + version = "0.1.8"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78"; }; + dependencies = { + autocfg = rustPackages."registry+https://github.com/rust-lang/crates.io-index".autocfg."1.1.0" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".autocfg."1.1.0" = overridableMkRustCrate (profileName: rec { name = "autocfg"; version = "1.1.0"; @@ -1364,6 +1374,7 @@ in futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; garage_api = rustPackages."unknown".garage_api."0.7.0" { inherit profileName; }; + garage_block = rustPackages."unknown".garage_block."0.7.0" { inherit profileName; }; garage_db = rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }; garage_model = rustPackages."unknown".garage_model."0.7.0" { inherit profileName; }; garage_rpc = rustPackages."unknown".garage_rpc."0.7.0" { inherit profileName; }; @@ -1383,6 +1394,7 @@ in serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; serde_bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; structopt = rustPackages."registry+https://github.com/rust-lang/crates.io-index".structopt."0.3.26" { inherit profileName; }; + timeago = rustPackages."registry+https://github.com/rust-lang/crates.io-index".timeago."0.3.1" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; toml = rustPackages."registry+https://github.com/rust-lang/crates.io-index".toml."0.5.8" { inherit profileName; }; tracing = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; @@ -1458,6 +1470,7 @@ in registry = "unknown"; src = fetchCrateLocal (workspaceSrc + "/src/block"); dependencies = { + arc_swap = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; @@ -1493,10 +1506,10 @@ in err_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; heed = rustPackages."registry+https://github.com/rust-lang/crates.io-index".heed."0.11.0" { inherit profileName; }; hexdump = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hexdump."0.1.1" { inherit profileName; }; - log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if rootFeatures' ? "garage_db" then "pretty_env_logger" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pretty_env_logger."0.4.0" { inherit profileName; }; rusqlite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rusqlite."0.27.0" { inherit profileName; }; sled = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; + tracing = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; }; devDependencies = { mktemp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".mktemp."0.4.1" { inherit profileName; }; @@ -1717,6 +1730,7 @@ in (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_util") "k2v") ]; dependencies = { + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "async_trait" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "blake2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".blake2."0.9.2" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "chrono" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.19" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "err_derive" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; @@ -2219,6 +2233,22 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".isolang."1.0.0" = overridableMkRustCrate (profileName: rec { + name = "isolang"; + version = "1.0.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "265ef164908329e47e753c769b14cbb27434abf0c41984dca201484022f09ce5"; }; + features = builtins.concatLists [ + [ "default" ] + ]; + dependencies = { + phf = rustPackages."registry+https://github.com/rust-lang/crates.io-index".phf."0.7.24" { inherit profileName; }; + }; + buildDependencies = { + phf_codegen = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".phf_codegen."0.7.24" { profileName = "__noProfile"; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".itertools."0.4.19" = overridableMkRustCrate (profileName: rec { name = "itertools"; version = "0.4.19"; @@ -3242,6 +3272,48 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".phf."0.7.24" = overridableMkRustCrate (profileName: rec { + name = "phf"; + version = "0.7.24"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18"; }; + dependencies = { + phf_shared = rustPackages."registry+https://github.com/rust-lang/crates.io-index".phf_shared."0.7.24" { inherit profileName; }; + }; + }); + + "registry+https://github.com/rust-lang/crates.io-index".phf_codegen."0.7.24" = overridableMkRustCrate (profileName: rec { + name = "phf_codegen"; + version = "0.7.24"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e"; }; + dependencies = { + phf_generator = rustPackages."registry+https://github.com/rust-lang/crates.io-index".phf_generator."0.7.24" { inherit profileName; }; + phf_shared = rustPackages."registry+https://github.com/rust-lang/crates.io-index".phf_shared."0.7.24" { inherit profileName; }; + }; + }); + + "registry+https://github.com/rust-lang/crates.io-index".phf_generator."0.7.24" = overridableMkRustCrate (profileName: rec { + name = "phf_generator"; + version = "0.7.24"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662"; }; + dependencies = { + phf_shared = rustPackages."registry+https://github.com/rust-lang/crates.io-index".phf_shared."0.7.24" { inherit profileName; }; + rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.6.5" { inherit profileName; }; + }; + }); + + "registry+https://github.com/rust-lang/crates.io-index".phf_shared."0.7.24" = overridableMkRustCrate (profileName: rec { + name = "phf_shared"; + version = "0.7.24"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0"; }; + dependencies = { + siphasher = rustPackages."registry+https://github.com/rust-lang/crates.io-index".siphasher."0.2.3" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".pin-project."0.4.29" = overridableMkRustCrate (profileName: rec { name = "pin-project"; version = "0.4.29"; @@ -3568,6 +3640,34 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".rand."0.6.5" = overridableMkRustCrate (profileName: rec { + name = "rand"; + version = "0.6.5"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca"; }; + features = builtins.concatLists [ + [ "alloc" ] + [ "default" ] + [ "rand_os" ] + [ "std" ] + ]; + dependencies = { + ${ if hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + rand_chacha = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_chacha."0.1.1" { inherit profileName; }; + rand_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_core."0.4.2" { inherit profileName; }; + rand_hc = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_hc."0.1.0" { inherit profileName; }; + rand_isaac = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_isaac."0.1.1" { inherit profileName; }; + rand_jitter = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_jitter."0.1.4" { inherit profileName; }; + rand_os = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_os."0.1.3" { inherit profileName; }; + rand_pcg = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_pcg."0.1.2" { inherit profileName; }; + rand_xorshift = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_xorshift."0.1.1" { inherit profileName; }; + ${ if hostPlatform.isWindows then "winapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".winapi."0.3.9" { inherit profileName; }; + }; + buildDependencies = { + autocfg = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".autocfg."0.1.8" { profileName = "__noProfile"; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" = overridableMkRustCrate (profileName: rec { name = "rand"; version = "0.8.5"; @@ -3590,6 +3690,19 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".rand_chacha."0.1.1" = overridableMkRustCrate (profileName: rec { + name = "rand_chacha"; + version = "0.1.1"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef"; }; + dependencies = { + rand_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_core."0.3.1" { inherit profileName; }; + }; + buildDependencies = { + autocfg = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".autocfg."0.1.8" { profileName = "__noProfile"; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".rand_chacha."0.3.1" = overridableMkRustCrate (profileName: rec { name = "rand_chacha"; version = "0.3.1"; @@ -3644,6 +3757,93 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".rand_hc."0.1.0" = overridableMkRustCrate (profileName: rec { + name = "rand_hc"; + version = "0.1.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4"; }; + dependencies = { + rand_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_core."0.3.1" { inherit profileName; }; + }; + }); + + "registry+https://github.com/rust-lang/crates.io-index".rand_isaac."0.1.1" = overridableMkRustCrate (profileName: rec { + name = "rand_isaac"; + version = "0.1.1"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08"; }; + dependencies = { + rand_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_core."0.3.1" { inherit profileName; }; + }; + }); + + "registry+https://github.com/rust-lang/crates.io-index".rand_jitter."0.1.4" = overridableMkRustCrate (profileName: rec { + name = "rand_jitter"; + version = "0.1.4"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b"; }; + features = builtins.concatLists [ + [ "std" ] + ]; + dependencies = { + ${ if hostPlatform.parsed.kernel.name == "darwin" || hostPlatform.parsed.kernel.name == "ios" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + rand_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_core."0.4.2" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "windows" then "winapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".winapi."0.3.9" { inherit profileName; }; + }; + }); + + "registry+https://github.com/rust-lang/crates.io-index".rand_os."0.1.3" = overridableMkRustCrate (profileName: rec { + name = "rand_os"; + version = "0.1.3"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071"; }; + dependencies = { + ${ if hostPlatform.parsed.kernel.name == "cloudabi" then "cloudabi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cloudabi."0.0.3" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "fuchsia" then "fuchsia_cprng" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".fuchsia-cprng."0.1.1" { inherit profileName; }; + ${ if hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + rand_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_core."0.4.2" { inherit profileName; }; + ${ if hostPlatform.parsed.abi.name == "sgx" then "rdrand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rdrand."0.4.0" { inherit profileName; }; + ${ if hostPlatform.isWindows then "winapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".winapi."0.3.9" { inherit profileName; }; + }; + }); + + "registry+https://github.com/rust-lang/crates.io-index".rand_pcg."0.1.2" = overridableMkRustCrate (profileName: rec { + name = "rand_pcg"; + version = "0.1.2"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44"; }; + dependencies = { + rand_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_core."0.4.2" { inherit profileName; }; + }; + buildDependencies = { + autocfg = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".autocfg."0.1.8" { profileName = "__noProfile"; }; + }; + }); + + "registry+https://github.com/rust-lang/crates.io-index".rand_xorshift."0.1.1" = overridableMkRustCrate (profileName: rec { + name = "rand_xorshift"; + version = "0.1.1"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c"; }; + dependencies = { + rand_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_core."0.3.1" { inherit profileName; }; + }; + }); + + "registry+https://github.com/rust-lang/crates.io-index".rdrand."0.4.0" = overridableMkRustCrate (profileName: rec { + name = "rdrand"; + version = "0.4.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"; }; + features = builtins.concatLists [ + [ "default" ] + [ "std" ] + ]; + dependencies = { + rand_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_core."0.3.1" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".redox_syscall."0.2.11" = overridableMkRustCrate (profileName: rec { name = "redox_syscall"; version = "0.2.11"; @@ -3738,7 +3938,7 @@ in ]; dependencies = { ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" || hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "i686" || hostPlatform.parsed.cpu.name == "x86_64" || (hostPlatform.parsed.cpu.name == "aarch64" || hostPlatform.parsed.cpu.name == "armv6l" || hostPlatform.parsed.cpu.name == "armv7l") && (hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "fuchsia" || hostPlatform.parsed.kernel.name == "linux") then "spin" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".spin."0.5.2" { inherit profileName; }; untrusted = rustPackages."registry+https://github.com/rust-lang/crates.io-index".untrusted."0.7.1" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "wasm32" && hostPlatform.parsed.vendor.name == "unknown" && hostPlatform.parsed.kernel.name == "unknown" && hostPlatform.parsed.abi.name == "" then "web_sys" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.56" { inherit profileName; }; @@ -4213,6 +4413,13 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".siphasher."0.2.3" = overridableMkRustCrate (profileName: rec { + name = "siphasher"; + version = "0.2.3"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"; }; + }); + "registry+https://github.com/rust-lang/crates.io-index".slab."0.4.5" = overridableMkRustCrate (profileName: rec { name = "slab"; version = "0.4.5"; @@ -4327,7 +4534,7 @@ in ]; dependencies = { bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot_core."0.8.5" { inherit profileName; }; static_init_macro = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init_macro."1.0.2" { profileName = "__noProfile"; }; @@ -4410,7 +4617,7 @@ in [ "proc-macro" ] [ "quote" ] [ "visit" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "visit-mut") + [ "visit-mut" ] ]; dependencies = { proc_macro2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.36" { inherit profileName; }; @@ -4539,6 +4746,23 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".timeago."0.3.1" = overridableMkRustCrate (profileName: rec { + name = "timeago"; + version = "0.3.1"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "6ec32dde57efb15c035ac074118d7f32820451395f28cb0524a01d4e94983b26"; }; + features = builtins.concatLists [ + [ "chrono" ] + [ "default" ] + [ "isolang" ] + [ "translations" ] + ]; + dependencies = { + chrono = rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.19" { inherit profileName; }; + isolang = rustPackages."registry+https://github.com/rust-lang/crates.io-index".isolang."1.0.0" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".tinyvec."1.5.1" = overridableMkRustCrate (profileName: rec { name = "tinyvec"; version = "1.5.1"; @@ -4883,19 +5107,19 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "4a1bdf54a7c28a2bbf701e1d2233f6c77f473486b94bee4f9678da5a148dca7f"; }; features = builtins.concatLists [ - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "attributes") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") + [ "attributes" ] + [ "default" ] (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "log") (lib.optional (rootFeatures' ? "garage") "log-always") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "std") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "tracing-attributes") + [ "std" ] + [ "tracing-attributes" ] ]; dependencies = { - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "cfg_if" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }; + cfg_if = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "log" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "pin_project_lite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tracing_attributes" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-attributes."0.1.20" { profileName = "__noProfile"; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tracing_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-core."0.1.23" { inherit profileName; }; + pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; + tracing_attributes = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-attributes."0.1.20" { profileName = "__noProfile"; }; + tracing_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-core."0.1.23" { inherit profileName; }; }; }); @@ -5330,10 +5554,10 @@ in [ "default" ] ]; dependencies = { - ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-uwp-windows-gnu" || hostPlatform.config == "i686-pc-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); diff --git a/Makefile b/Makefile index eeeffedb..1f0f3644 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,27 @@ -.PHONY: doc all release shell +.PHONY: doc all release shell run1 run2 run3 all: clear; cargo build --all-features -doc: - cd doc/book; mdbook build - release: nix-build --arg release true shell: nix-shell + +# ---- + +run1: + RUST_LOG=garage=debug ./target/debug/garage -c tmp/config1.toml server +run1rel: + RUST_LOG=garage=debug ./target/release/garage -c tmp/config1.toml server + +run2: + RUST_LOG=garage=debug ./target/debug/garage -c tmp/config2.toml server +run2rel: + RUST_LOG=garage=debug ./target/release/garage -c tmp/config2.toml server + +run3: + RUST_LOG=garage=debug ./target/debug/garage -c tmp/config3.toml server +run3rel: + RUST_LOG=garage=debug ./target/release/garage -c tmp/config3.toml server diff --git a/src/block/Cargo.toml b/src/block/Cargo.toml index 80346aca..2555a44a 100644 --- a/src/block/Cargo.toml +++ b/src/block/Cargo.toml @@ -21,6 +21,7 @@ garage_table = { version = "0.7.0", path = "../table" } opentelemetry = "0.17" +arc-swap = "1.5" async-trait = "0.1.7" bytes = "1.0" hex = "0.4" diff --git a/src/block/lib.rs b/src/block/lib.rs index dc685657..ebdb95d8 100644 --- a/src/block/lib.rs +++ b/src/block/lib.rs @@ -2,6 +2,7 @@ extern crate tracing; pub mod manager; +pub mod repair; mod block; mod metrics; diff --git a/src/block/manager.rs b/src/block/manager.rs index 32ba0431..017ba9da 100644 --- a/src/block/manager.rs +++ b/src/block/manager.rs @@ -1,18 +1,17 @@ -use core::ops::Bound; - use std::convert::TryInto; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; +use arc_swap::ArcSwapOption; use async_trait::async_trait; use serde::{Deserialize, Serialize}; use futures::future::*; -use futures::select; use tokio::fs; use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::sync::{watch, Mutex, Notify}; +use tokio::select; +use tokio::sync::{mpsc, watch, Mutex, Notify}; use opentelemetry::{ trace::{FutureExt as OtelFutureExt, TraceContextExt, Tracer}, @@ -22,6 +21,7 @@ use opentelemetry::{ use garage_db as db; use garage_db::counted_tree_hack::CountedTree; +use garage_util::background::*; use garage_util::data::*; use garage_util::error::*; use garage_util::metrics::RecordDuration; @@ -36,6 +36,7 @@ use garage_table::replication::{TableReplication, TableShardedReplication}; use crate::block::*; use crate::metrics::*; use crate::rc::*; +use crate::repair::*; /// Size under which data will be stored inlined in database instead of as files pub const INLINE_THRESHOLD: usize = 3072; @@ -93,16 +94,18 @@ pub struct BlockManager { mutation_lock: Mutex, - rc: BlockRc, + pub(crate) rc: BlockRc, resync_queue: CountedTree, resync_notify: Notify, resync_errors: CountedTree, - system: Arc, + pub(crate) system: Arc, endpoint: Arc>, metrics: BlockManagerMetrics, + + tx_scrub_command: ArcSwapOption>, } // This custom struct contains functions that must only be ran @@ -110,6 +113,12 @@ pub struct BlockManager { // it INSIDE a Mutex. struct BlockManagerLocked(); +enum ResyncIterResult { + BusyDidSomething, + BusyDidNothing, + IdleFor(Duration), +} + impl BlockManager { pub fn new( db: &db::Db, @@ -157,10 +166,11 @@ impl BlockManager { system, endpoint, metrics, + tx_scrub_command: ArcSwapOption::new(None), }); block_manager.endpoint.set_handler(block_manager.clone()); - block_manager.clone().spawn_background_worker(); + block_manager.clone().spawn_background_workers(); block_manager } @@ -218,90 +228,6 @@ impl BlockManager { Ok(()) } - /// Launch the repair procedure on the data store - /// - /// This will list all blocks locally present, as well as those - /// that are required because of refcount > 0, and will try - /// to fix any mismatch between the two. - pub async fn repair_data_store(&self, must_exit: &watch::Receiver) -> Result<(), Error> { - // 1. Repair blocks from RC table. - let mut next_start: Option = None; - loop { - // We have to do this complicated two-step process where we first read a bunch - // of hashes from the RC table, and then insert them in the to-resync queue, - // because of SQLite. Basically, as long as we have an iterator on a DB table, - // we can't do anything else on the DB. The naive approach (which we had previously) - // of just iterating on the RC table and inserting items one to one in the resync - // queue can't work here, it would just provoke a deadlock in the SQLite adapter code. - // This is mostly because the Rust bindings for SQLite assume a worst-case scenario - // where SQLite is not compiled in thread-safe mode, so we have to wrap everything - // in a mutex (see db/sqlite_adapter.rs and discussion in PR #322). - let mut batch_of_hashes = vec![]; - let start_bound = match next_start.as_ref() { - None => Bound::Unbounded, - Some(x) => Bound::Excluded(x.as_slice()), - }; - for entry in self - .rc - .rc - .range::<&[u8], _>((start_bound, Bound::Unbounded))? - { - let (hash, _) = entry?; - let hash = Hash::try_from(&hash[..]).unwrap(); - batch_of_hashes.push(hash); - if batch_of_hashes.len() >= 1000 { - break; - } - } - if batch_of_hashes.is_empty() { - break; - } - - for hash in batch_of_hashes.into_iter() { - self.put_to_resync(&hash, Duration::from_secs(0))?; - next_start = Some(hash) - } - - if *must_exit.borrow() { - return Ok(()); - } - } - - // 2. Repair blocks actually on disk - // Lists all blocks on disk and adds them to the resync queue. - // This allows us to find blocks we are storing but don't actually need, - // so that we can offload them if necessary and then delete them locally. - self.for_each_file( - (), - move |_, hash| async move { - self.put_to_resync(&hash, Duration::from_secs(0)) - .map_err(Into::into) - }, - must_exit, - ) - .await - } - - /// Verify integrity of each block on disk. Use `speed_limit` to limit the load generated by - /// this function. - pub async fn scrub_data_store( - &self, - must_exit: &watch::Receiver, - tranquility: u32, - ) -> Result<(), Error> { - let tranquilizer = Tranquilizer::new(30); - self.for_each_file( - tranquilizer, - move |mut tranquilizer, hash| async move { - let _ = self.read_block(&hash).await; - tranquilizer.tranquilize(tranquility).await; - Ok(tranquilizer) - }, - must_exit, - ) - .await - } - /// Get lenght of resync queue pub fn resync_queue_len(&self) -> Result { // This currently can't return an error because the CountedTree hack @@ -321,6 +247,17 @@ impl BlockManager { Ok(self.rc.rc.len()?) } + /// Send command to start/stop/manager scrub worker + pub async fn send_scrub_command(&self, cmd: ScrubWorkerCommand) { + let _ = self + .tx_scrub_command + .load() + .as_ref() + .unwrap() + .send(cmd) + .await; + } + //// ----- Managing the reference counter ---- /// Increment the number of time a block is used, putting it to resynchronization if it is @@ -390,7 +327,7 @@ impl BlockManager { } /// Read block from disk, verifying it's integrity - async fn read_block(&self, hash: &Hash) -> Result { + pub(crate) async fn read_block(&self, hash: &Hash) -> Result { let data = self .read_block_internal(hash) .bound_record_duration(&self.metrics.block_read_duration) @@ -554,18 +491,23 @@ impl BlockManager { // for times that are earlier than the exponential back-off delay // is a natural condition that is handled properly). - fn spawn_background_worker(self: Arc) { + fn spawn_background_workers(self: Arc) { // Launch a background workers for background resync loop processing let background = self.system.background.clone(); + let worker = ResyncWorker::new(self.clone()); tokio::spawn(async move { tokio::time::sleep(Duration::from_secs(10)).await; - background.spawn_worker("block resync worker".into(), move |must_exit| { - self.resync_loop(must_exit) - }); + background.spawn_worker(worker); }); + + // Launch a background worker for data store scrubs + let (scrub_tx, scrub_rx) = mpsc::channel(1); + self.tx_scrub_command.store(Some(Arc::new(scrub_tx))); + let scrub_worker = ScrubWorker::new(self.clone(), scrub_rx); + self.system.background.spawn_worker(scrub_worker); } - fn put_to_resync(&self, hash: &Hash, delay: Duration) -> db::Result<()> { + pub(crate) fn put_to_resync(&self, hash: &Hash, delay: Duration) -> db::Result<()> { let when = now_msec() + delay.as_millis() as u64; self.put_to_resync_at(hash, when) } @@ -579,37 +521,7 @@ impl BlockManager { Ok(()) } - async fn resync_loop(self: Arc, mut must_exit: watch::Receiver) { - let mut tranquilizer = Tranquilizer::new(30); - - while !*must_exit.borrow() { - match self.resync_iter(&mut must_exit).await { - Ok(true) => { - tranquilizer.tranquilize(self.background_tranquility).await; - } - Ok(false) => { - tranquilizer.reset(); - } - Err(e) => { - // The errors that we have here are only Sled errors - // We don't really know how to handle them so just ¯\_(ツ)_/¯ - // (there is kind of an assumption that Sled won't error on us, - // if it does there is not much we can do -- TODO should we just panic?) - error!( - "Could not do a resync iteration: {} (this is a very bad error)", - e - ); - tranquilizer.reset(); - } - } - } - } - - // The result of resync_iter is: - // - Ok(true) -> a block was processed (successfully or not) - // - Ok(false) -> no block was processed, but we are ready for the next iteration - // - Err(_) -> a Sled error occurred when reading/writing from resync_queue/resync_errors - async fn resync_iter(&self, must_exit: &mut watch::Receiver) -> Result { + async fn resync_iter(&self) -> Result { if let Some((time_bytes, hash_bytes)) = self.resync_queue.first()? { let time_msec = u64::from_be_bytes(time_bytes[0..8].try_into().unwrap()); let now = now_msec(); @@ -629,7 +541,7 @@ impl BlockManager { // (we want to do the remove after the insert to ensure // that the item is not lost if we crash in-between) self.resync_queue.remove(time_bytes)?; - return Ok(false); + return Ok(ResyncIterResult::BusyDidNothing); } } @@ -676,15 +588,11 @@ impl BlockManager { self.resync_queue.remove(time_bytes)?; } - Ok(true) + Ok(ResyncIterResult::BusyDidSomething) } else { - let delay = tokio::time::sleep(Duration::from_millis(time_msec - now)); - select! { - _ = delay.fuse() => {}, - _ = self.resync_notify.notified().fuse() => {}, - _ = must_exit.changed().fuse() => {}, - } - Ok(false) + Ok(ResyncIterResult::IdleFor(Duration::from_millis( + time_msec - now, + ))) } } else { // Here we wait either for a notification that an item has been @@ -693,13 +601,7 @@ impl BlockManager { // between the time we checked the queue and the first poll // to resync_notify.notified(): if that happens, we'll just loop // back 10 seconds later, which is fine. - let delay = tokio::time::sleep(Duration::from_secs(10)); - select! { - _ = delay.fuse() => {}, - _ = self.resync_notify.notified().fuse() => {}, - _ = must_exit.changed().fuse() => {}, - } - Ok(false) + Ok(ResyncIterResult::IdleFor(Duration::from_secs(10))) } } @@ -814,72 +716,6 @@ impl BlockManager { Ok(()) } - - // ---- Utility: iteration on files in the data directory ---- - - async fn for_each_file( - &self, - state: State, - mut f: F, - must_exit: &watch::Receiver, - ) -> Result<(), Error> - where - F: FnMut(State, Hash) -> Fut + Send, - Fut: Future> + Send, - State: Send, - { - self.for_each_file_rec(&self.data_dir, state, &mut f, must_exit) - .await - .map(|_| ()) - } - - fn for_each_file_rec<'a, F, Fut, State>( - &'a self, - path: &'a Path, - mut state: State, - f: &'a mut F, - must_exit: &'a watch::Receiver, - ) -> BoxFuture<'a, Result> - where - F: FnMut(State, Hash) -> Fut + Send, - Fut: Future> + Send, - State: Send + 'a, - { - async move { - let mut ls_data_dir = fs::read_dir(path).await?; - while let Some(data_dir_ent) = ls_data_dir.next_entry().await? { - if *must_exit.borrow() { - break; - } - - let name = data_dir_ent.file_name(); - let name = if let Ok(n) = name.into_string() { - n - } else { - continue; - }; - let ent_type = data_dir_ent.file_type().await?; - - let name = name.strip_suffix(".zst").unwrap_or(&name); - if name.len() == 2 && hex::decode(&name).is_ok() && ent_type.is_dir() { - state = self - .for_each_file_rec(&data_dir_ent.path(), state, f, must_exit) - .await?; - } else if name.len() == 64 { - let hash_bytes = if let Ok(h) = hex::decode(&name) { - h - } else { - continue; - }; - let mut hash = [0u8; 32]; - hash.copy_from_slice(&hash_bytes[..]); - state = f(state, hash.into()).await?; - } - } - Ok(state) - } - .boxed() - } } #[async_trait] @@ -898,6 +734,77 @@ impl EndpointHandler for BlockManager { } } +struct ResyncWorker { + manager: Arc, + tranquilizer: Tranquilizer, + next_delay: Duration, +} + +impl ResyncWorker { + fn new(manager: Arc) -> Self { + Self { + manager, + tranquilizer: Tranquilizer::new(30), + next_delay: Duration::from_secs(10), + } + } +} + +#[async_trait] +impl Worker for ResyncWorker { + fn name(&self) -> String { + "Block resync worker".into() + } + + fn info(&self) -> Option { + let mut ret = vec![]; + let qlen = self.manager.resync_queue_len().unwrap_or(0); + let elen = self.manager.resync_errors_len().unwrap_or(0); + if qlen > 0 { + ret.push(format!("{} blocks in queue", qlen)); + } + if elen > 0 { + ret.push(format!("{} blocks in error state", elen)); + } + if !ret.is_empty() { + Some(ret.join(", ")) + } else { + None + } + } + + async fn work(&mut self, _must_exit: &mut watch::Receiver) -> Result { + self.tranquilizer.reset(); + match self.manager.resync_iter().await { + Ok(ResyncIterResult::BusyDidSomething) => Ok(self + .tranquilizer + .tranquilize_worker(self.manager.background_tranquility)), + Ok(ResyncIterResult::BusyDidNothing) => Ok(WorkerState::Busy), + Ok(ResyncIterResult::IdleFor(delay)) => { + self.next_delay = delay; + Ok(WorkerState::Idle) + } + Err(e) => { + // The errors that we have here are only Sled errors + // We don't really know how to handle them so just ¯\_(ツ)_/¯ + // (there is kind of an assumption that Sled won't error on us, + // if it does there is not much we can do -- TODO should we just panic?) + // Here we just give the error to the worker manager, + // it will print it to the logs and increment a counter + Err(e.into()) + } + } + } + + async fn wait_for_work(&mut self, _must_exit: &watch::Receiver) -> WorkerState { + select! { + _ = tokio::time::sleep(self.next_delay) => (), + _ = self.manager.resync_notify.notified() => (), + }; + WorkerState::Busy + } +} + struct BlockStatus { exists: bool, needed: RcEntry, diff --git a/src/block/repair.rs b/src/block/repair.rs new file mode 100644 index 00000000..07ff6772 --- /dev/null +++ b/src/block/repair.rs @@ -0,0 +1,444 @@ +use core::ops::Bound; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; + +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use tokio::fs; +use tokio::select; +use tokio::sync::mpsc; +use tokio::sync::watch; + +use garage_util::background::*; +use garage_util::data::*; +use garage_util::error::*; +use garage_util::persister::Persister; +use garage_util::time::*; +use garage_util::tranquilizer::Tranquilizer; + +use crate::manager::*; + +const SCRUB_INTERVAL: Duration = Duration::from_secs(3600 * 24 * 30); // full scrub every 30 days + +pub struct RepairWorker { + manager: Arc, + next_start: Option, + block_iter: Option, +} + +impl RepairWorker { + pub fn new(manager: Arc) -> Self { + Self { + manager, + next_start: None, + block_iter: None, + } + } +} + +#[async_trait] +impl Worker for RepairWorker { + fn name(&self) -> String { + "Block repair worker".into() + } + + fn info(&self) -> Option { + match self.block_iter.as_ref() { + None => { + let idx_bytes = self + .next_start + .as_ref() + .map(|x| x.as_slice()) + .unwrap_or(&[]); + let idx_bytes = if idx_bytes.len() > 4 { + &idx_bytes[..4] + } else { + idx_bytes + }; + Some(format!("Phase 1: {}", hex::encode(idx_bytes))) + } + Some(bi) => Some(format!("Phase 2: {:.2}% done", bi.progress() * 100.)), + } + } + + async fn work(&mut self, _must_exit: &mut watch::Receiver) -> Result { + match self.block_iter.as_mut() { + None => { + // Phase 1: Repair blocks from RC table. + + // We have to do this complicated two-step process where we first read a bunch + // of hashes from the RC table, and then insert them in the to-resync queue, + // because of SQLite. Basically, as long as we have an iterator on a DB table, + // we can't do anything else on the DB. The naive approach (which we had previously) + // of just iterating on the RC table and inserting items one to one in the resync + // queue can't work here, it would just provoke a deadlock in the SQLite adapter code. + // This is mostly because the Rust bindings for SQLite assume a worst-case scenario + // where SQLite is not compiled in thread-safe mode, so we have to wrap everything + // in a mutex (see db/sqlite_adapter.rs and discussion in PR #322). + // TODO: maybe do this with tokio::task::spawn_blocking ? + let mut batch_of_hashes = vec![]; + let start_bound = match self.next_start.as_ref() { + None => Bound::Unbounded, + Some(x) => Bound::Excluded(x.as_slice()), + }; + for entry in self + .manager + .rc + .rc + .range::<&[u8], _>((start_bound, Bound::Unbounded))? + { + let (hash, _) = entry?; + let hash = Hash::try_from(&hash[..]).unwrap(); + batch_of_hashes.push(hash); + if batch_of_hashes.len() >= 1000 { + break; + } + } + if batch_of_hashes.is_empty() { + // move on to phase 2 + self.block_iter = Some(BlockStoreIterator::new(&self.manager)); + return Ok(WorkerState::Busy); + } + + for hash in batch_of_hashes.into_iter() { + self.manager.put_to_resync(&hash, Duration::from_secs(0))?; + self.next_start = Some(hash) + } + + Ok(WorkerState::Busy) + } + Some(bi) => { + // Phase 2: Repair blocks actually on disk + // Lists all blocks on disk and adds them to the resync queue. + // This allows us to find blocks we are storing but don't actually need, + // so that we can offload them if necessary and then delete them locally. + if let Some(hash) = bi.next().await? { + self.manager.put_to_resync(&hash, Duration::from_secs(0))?; + Ok(WorkerState::Busy) + } else { + Ok(WorkerState::Done) + } + } + } + } + + async fn wait_for_work(&mut self, _must_exit: &watch::Receiver) -> WorkerState { + unreachable!() + } +} + +// ---- + +pub struct ScrubWorker { + manager: Arc, + rx_cmd: mpsc::Receiver, + + work: ScrubWorkerState, + tranquilizer: Tranquilizer, + + persister: Persister, + persisted: ScrubWorkerPersisted, +} + +#[derive(Serialize, Deserialize)] +struct ScrubWorkerPersisted { + tranquility: u32, + time_last_complete_scrub: u64, + corruptions_detected: u64, +} + +enum ScrubWorkerState { + Running(BlockStoreIterator), + Paused(BlockStoreIterator, u64), // u64 = time when to resume scrub + Finished, +} + +impl Default for ScrubWorkerState { + fn default() -> Self { + ScrubWorkerState::Finished + } +} + +#[derive(Debug)] +pub enum ScrubWorkerCommand { + Start, + Pause(Duration), + Resume, + Cancel, + SetTranquility(u32), +} + +impl ScrubWorker { + pub fn new(manager: Arc, rx_cmd: mpsc::Receiver) -> Self { + let persister = Persister::new(&manager.system.metadata_dir, "scrub_info"); + let persisted = match persister.load() { + Ok(v) => v, + Err(_) => ScrubWorkerPersisted { + time_last_complete_scrub: 0, + tranquility: 4, + corruptions_detected: 0, + }, + }; + Self { + manager, + rx_cmd, + work: ScrubWorkerState::Finished, + tranquilizer: Tranquilizer::new(30), + persister, + persisted, + } + } + + async fn handle_cmd(&mut self, cmd: ScrubWorkerCommand) { + match cmd { + ScrubWorkerCommand::Start => { + self.work = match std::mem::take(&mut self.work) { + ScrubWorkerState::Finished => { + let iterator = BlockStoreIterator::new(&self.manager); + ScrubWorkerState::Running(iterator) + } + work => { + error!("Cannot start scrub worker: already running!"); + work + } + }; + } + ScrubWorkerCommand::Pause(dur) => { + self.work = match std::mem::take(&mut self.work) { + ScrubWorkerState::Running(it) | ScrubWorkerState::Paused(it, _) => { + ScrubWorkerState::Paused(it, now_msec() + dur.as_millis() as u64) + } + work => { + error!("Cannot pause scrub worker: not running!"); + work + } + }; + } + ScrubWorkerCommand::Resume => { + self.work = match std::mem::take(&mut self.work) { + ScrubWorkerState::Paused(it, _) => ScrubWorkerState::Running(it), + work => { + error!("Cannot resume scrub worker: not paused!"); + work + } + }; + } + ScrubWorkerCommand::Cancel => { + self.work = match std::mem::take(&mut self.work) { + ScrubWorkerState::Running(_) | ScrubWorkerState::Paused(_, _) => { + ScrubWorkerState::Finished + } + work => { + error!("Cannot cancel scrub worker: not running!"); + work + } + } + } + ScrubWorkerCommand::SetTranquility(t) => { + self.persisted.tranquility = t; + if let Err(e) = self.persister.save_async(&self.persisted).await { + error!("Could not save new tranquilitiy value: {}", e); + } + } + } + } +} + +#[async_trait] +impl Worker for ScrubWorker { + fn name(&self) -> String { + "Block scrub worker".into() + } + + fn info(&self) -> Option { + let s = match &self.work { + ScrubWorkerState::Running(bsi) => format!( + "{:.2}% done (tranquility = {})", + bsi.progress() * 100., + self.persisted.tranquility + ), + ScrubWorkerState::Paused(bsi, rt) => { + format!( + "Paused, {:.2}% done, resumes at {}", + bsi.progress() * 100., + msec_to_rfc3339(*rt) + ) + } + ScrubWorkerState::Finished => format!( + "Last completed scrub: {}", + msec_to_rfc3339(self.persisted.time_last_complete_scrub) + ), + }; + Some(format!( + "{} ; corruptions detected: {}", + s, self.persisted.corruptions_detected + )) + } + + async fn work(&mut self, _must_exit: &mut watch::Receiver) -> Result { + match self.rx_cmd.try_recv() { + Ok(cmd) => self.handle_cmd(cmd).await, + Err(mpsc::error::TryRecvError::Disconnected) => return Ok(WorkerState::Done), + Err(mpsc::error::TryRecvError::Empty) => (), + }; + + match &mut self.work { + ScrubWorkerState::Running(bsi) => { + self.tranquilizer.reset(); + if let Some(hash) = bsi.next().await? { + match self.manager.read_block(&hash).await { + Err(Error::CorruptData(_)) => { + error!("Found corrupt data block during scrub: {:?}", hash); + self.persisted.corruptions_detected += 1; + self.persister.save_async(&self.persisted).await?; + } + Err(e) => return Err(e), + _ => (), + }; + Ok(self + .tranquilizer + .tranquilize_worker(self.persisted.tranquility)) + } else { + self.persisted.time_last_complete_scrub = now_msec(); + self.persister.save_async(&self.persisted).await?; + self.work = ScrubWorkerState::Finished; + self.tranquilizer.clear(); + Ok(WorkerState::Idle) + } + } + _ => Ok(WorkerState::Idle), + } + } + + async fn wait_for_work(&mut self, _must_exit: &watch::Receiver) -> WorkerState { + let (wait_until, command) = match &self.work { + ScrubWorkerState::Running(_) => return WorkerState::Busy, + ScrubWorkerState::Paused(_, resume_time) => (*resume_time, ScrubWorkerCommand::Resume), + ScrubWorkerState::Finished => ( + self.persisted.time_last_complete_scrub + SCRUB_INTERVAL.as_millis() as u64, + ScrubWorkerCommand::Start, + ), + }; + + let now = now_msec(); + if now >= wait_until { + self.handle_cmd(command).await; + return WorkerState::Busy; + } + let delay = Duration::from_millis(wait_until - now); + select! { + _ = tokio::time::sleep(delay) => self.handle_cmd(command).await, + cmd = self.rx_cmd.recv() => if let Some(cmd) = cmd { + self.handle_cmd(cmd).await; + } else { + return WorkerState::Done; + } + } + + match &self.work { + ScrubWorkerState::Running(_) => WorkerState::Busy, + _ => WorkerState::Idle, + } + } +} + +// ---- + +struct BlockStoreIterator { + path: Vec, +} + +enum ReadingDir { + Pending(PathBuf), + Read { + subpaths: Vec, + pos: usize, + }, +} + +impl BlockStoreIterator { + fn new(manager: &BlockManager) -> Self { + let root_dir = manager.data_dir.clone(); + Self { + path: vec![ReadingDir::Pending(root_dir)], + } + } + + /// Returns progress done, between 0 and 1 + fn progress(&self) -> f32 { + if self.path.is_empty() { + 1.0 + } else { + let mut ret = 0.0; + let mut next_div = 1; + for p in self.path.iter() { + match p { + ReadingDir::Pending(_) => break, + ReadingDir::Read { subpaths, pos } => { + next_div *= subpaths.len(); + ret += ((*pos - 1) as f32) / (next_div as f32); + } + } + } + ret + } + } + + async fn next(&mut self) -> Result, Error> { + loop { + let last_path = match self.path.last_mut() { + None => return Ok(None), + Some(lp) => lp, + }; + + if let ReadingDir::Pending(path) = last_path { + let mut reader = fs::read_dir(&path).await?; + let mut subpaths = vec![]; + while let Some(ent) = reader.next_entry().await? { + subpaths.push(ent); + } + *last_path = ReadingDir::Read { subpaths, pos: 0 }; + } + + let (subpaths, pos) = match *last_path { + ReadingDir::Read { + ref subpaths, + ref mut pos, + } => (subpaths, pos), + ReadingDir::Pending(_) => unreachable!(), + }; + + let data_dir_ent = match subpaths.get(*pos) { + None => { + self.path.pop(); + continue; + } + Some(ent) => { + *pos += 1; + ent + } + }; + + let name = data_dir_ent.file_name(); + let name = if let Ok(n) = name.into_string() { + n + } else { + continue; + }; + let ent_type = data_dir_ent.file_type().await?; + + let name = name.strip_suffix(".zst").unwrap_or(&name); + if name.len() == 2 && hex::decode(&name).is_ok() && ent_type.is_dir() { + let path = data_dir_ent.path(); + self.path.push(ReadingDir::Pending(path)); + } else if name.len() == 64 { + if let Ok(h) = hex::decode(&name) { + let mut hash = [0u8; 32]; + hash.copy_from_slice(&h); + return Ok(Some(hash.into())); + } + } + } + } +} diff --git a/src/db/Cargo.toml b/src/db/Cargo.toml index 6d8f64be..f697054b 100644 --- a/src/db/Cargo.toml +++ b/src/db/Cargo.toml @@ -19,7 +19,7 @@ required-features = ["cli"] [dependencies] err-derive = "0.3" hexdump = "0.1" -log = "0.4" +tracing = "0.1.30" heed = "0.11" rusqlite = { version = "0.27", features = ["bundled"] } diff --git a/src/db/sqlite_adapter.rs b/src/db/sqlite_adapter.rs index 68d96ca0..97a78b07 100644 --- a/src/db/sqlite_adapter.rs +++ b/src/db/sqlite_adapter.rs @@ -6,7 +6,7 @@ use std::pin::Pin; use std::ptr::NonNull; use std::sync::{Arc, Mutex, MutexGuard}; -use log::trace; +use tracing::trace; use rusqlite::{params, Connection, Rows, Statement, Transaction}; diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index 640e6975..8948e750 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -23,6 +23,7 @@ path = "tests/lib.rs" [dependencies] garage_db = { version = "0.8.0", path = "../db" } garage_api = { version = "0.7.0", path = "../api" } +garage_block = { version = "0.7.0", path = "../block" } garage_model = { version = "0.7.0", path = "../model" } garage_rpc = { version = "0.7.0", path = "../rpc" } garage_table = { version = "0.7.0", path = "../table" } @@ -31,6 +32,7 @@ garage_web = { version = "0.7.0", path = "../web" } bytes = "1.0" bytesize = "1.1" +timeago = "0.3" hex = "0.4" tracing = { version = "0.1.30", features = ["log-always"] } pretty_env_logger = "0.4" diff --git a/src/garage/admin.rs b/src/garage/admin.rs index 48914655..71ee608c 100644 --- a/src/garage/admin.rs +++ b/src/garage/admin.rs @@ -24,7 +24,7 @@ use garage_model::migrate::Migrate; use garage_model::permission::*; use crate::cli::*; -use crate::repair::online::OnlineRepair; +use crate::repair::online::launch_online_repair; pub const ADMIN_RPC_PATH: &str = "garage/admin_rpc.rs/Rpc"; @@ -36,6 +36,7 @@ pub enum AdminRpc { LaunchRepair(RepairOpt), Migrate(MigrateOpt), Stats(StatsOpt), + Worker(WorkerOpt), // Replies Ok(String), @@ -47,6 +48,10 @@ pub enum AdminRpc { }, KeyList(Vec<(String, String)>), KeyInfo(Key, HashMap), + WorkerList( + HashMap, + WorkerListOpt, + ), } impl Rpc for AdminRpc { @@ -693,15 +698,7 @@ impl AdminRpcHandler { ))) } } else { - let repair = OnlineRepair { - garage: self.garage.clone(), - }; - self.garage - .system - .background - .spawn_worker("Repair worker".into(), move |must_exit| async move { - repair.repair_worker(opt, must_exit).await - }); + launch_online_repair(self.garage.clone(), opt).await; Ok(AdminRpc::Ok(format!( "Repair launched on {:?}", self.garage.system.id @@ -830,6 +827,17 @@ impl AdminRpcHandler { Ok(()) } + + // ---- + + async fn handle_worker_cmd(&self, opt: WorkerOpt) -> Result { + match opt.cmd { + WorkerCmd::List { opt } => { + let workers = self.garage.background.get_worker_info(); + Ok(AdminRpc::WorkerList(workers, opt)) + } + } + } } #[async_trait] @@ -845,6 +853,7 @@ impl EndpointHandler for AdminRpcHandler { AdminRpc::Migrate(opt) => self.handle_migrate(opt.clone()).await, AdminRpc::LaunchRepair(opt) => self.handle_launch_repair(opt.clone()).await, AdminRpc::Stats(opt) => self.handle_stats(opt.clone()).await, + AdminRpc::Worker(opt) => self.handle_worker_cmd(opt.clone()).await, m => Err(GarageError::unexpected_rpc_message(m).into()), } } diff --git a/src/garage/cli/cmd.rs b/src/garage/cli/cmd.rs index 3a0bd956..1aa2c2ff 100644 --- a/src/garage/cli/cmd.rs +++ b/src/garage/cli/cmd.rs @@ -1,4 +1,5 @@ use std::collections::HashSet; +use std::time::Duration; use garage_util::error::*; use garage_util::formater::format_table; @@ -39,6 +40,7 @@ pub async fn cli_command_dispatch( cmd_admin(admin_rpc_endpoint, rpc_host, AdminRpc::LaunchRepair(ro)).await } Command::Stats(so) => cmd_admin(admin_rpc_endpoint, rpc_host, AdminRpc::Stats(so)).await, + Command::Worker(wo) => cmd_admin(admin_rpc_endpoint, rpc_host, AdminRpc::Worker(wo)).await, _ => unreachable!(), } } @@ -100,6 +102,7 @@ pub async fn cmd_status(rpc_cli: &Endpoint, rpc_host: NodeID) -> vec!["ID\tHostname\tAddress\tTags\tZone\tCapacity\tLast seen".to_string()]; for adv in status.iter().filter(|adv| !adv.is_up) { if let Some(NodeRoleV(Some(cfg))) = layout.roles.get(&adv.id) { + let tf = timeago::Formatter::new(); failed_nodes.push(format!( "{id:?}\t{host}\t{addr}\t[{tags}]\t{zone}\t{capacity}\t{last_seen}", id = adv.id, @@ -110,7 +113,7 @@ pub async fn cmd_status(rpc_cli: &Endpoint, rpc_host: NodeID) -> capacity = cfg.capacity_string(), last_seen = adv .last_seen_secs_ago - .map(|s| format!("{}s ago", s)) + .map(|s| tf.convert(Duration::from_secs(s))) .unwrap_or_else(|| "never seen".into()), )); } @@ -182,6 +185,9 @@ pub async fn cmd_admin( AdminRpc::KeyInfo(key, rb) => { print_key_info(&key, &rb); } + AdminRpc::WorkerList(wi, wlo) => { + print_worker_info(wi, wlo); + } r => { error!("Unexpected response: {:?}", r); } diff --git a/src/garage/cli/structs.rs b/src/garage/cli/structs.rs index 4f2efe19..bc44b5ef 100644 --- a/src/garage/cli/structs.rs +++ b/src/garage/cli/structs.rs @@ -45,6 +45,10 @@ pub enum Command { /// Gather node statistics #[structopt(name = "stats")] Stats(StatsOpt), + + /// Manage background workers + #[structopt(name = "worker")] + Worker(WorkerOpt), } #[derive(StructOpt, Debug)] @@ -423,8 +427,29 @@ pub enum RepairWhat { /// Verify integrity of all blocks on disc (extremely slow, i/o intensive) #[structopt(name = "scrub")] Scrub { - /// Tranquility factor (see tranquilizer documentation) - #[structopt(name = "tranquility", default_value = "2")] + #[structopt(subcommand)] + cmd: ScrubCmd, + }, +} + +#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)] +pub enum ScrubCmd { + /// Start scrub + #[structopt(name = "start")] + Start, + /// Pause scrub (it will resume automatically after 24 hours) + #[structopt(name = "pause")] + Pause, + /// Resume paused scrub + #[structopt(name = "resume")] + Resume, + /// Cancel scrub in progress + #[structopt(name = "cancel")] + Cancel, + /// Set tranquility level for in-progress and future scrubs + #[structopt(name = "set-tranquility")] + SetTranquility { + #[structopt()] tranquility: u32, }, } @@ -460,3 +485,29 @@ pub struct StatsOpt { #[structopt(short = "d", long = "detailed")] pub detailed: bool, } + +#[derive(Serialize, Deserialize, StructOpt, Debug, Clone)] +pub struct WorkerOpt { + #[structopt(subcommand)] + pub cmd: WorkerCmd, +} + +#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)] +pub enum WorkerCmd { + /// List all workers on Garage node + #[structopt(name = "list")] + List { + #[structopt(flatten)] + opt: WorkerListOpt, + }, +} + +#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone, Copy)] +pub struct WorkerListOpt { + /// Show only busy workers + #[structopt(short = "b", long = "busy")] + pub busy: bool, + /// Show only workers with errors + #[structopt(short = "e", long = "errors")] + pub errors: bool, +} diff --git a/src/garage/cli/util.rs b/src/garage/cli/util.rs index 329e8a3e..396938ae 100644 --- a/src/garage/cli/util.rs +++ b/src/garage/cli/util.rs @@ -1,14 +1,19 @@ use std::collections::HashMap; +use std::time::Duration; +use garage_util::background::*; use garage_util::crdt::*; use garage_util::data::Uuid; use garage_util::error::*; use garage_util::formater::format_table; +use garage_util::time::*; use garage_model::bucket_table::*; use garage_model::key_table::*; use garage_model::s3::object_table::{BYTES, OBJECTS, UNFINISHED_UPLOADS}; +use crate::cli::structs::WorkerListOpt; + pub fn print_bucket_list(bl: Vec) { println!("List of buckets:"); @@ -235,3 +240,56 @@ pub fn find_matching_node( Ok(candidates[0]) } } + +pub fn print_worker_info(wi: HashMap, wlo: WorkerListOpt) { + let mut wi = wi.into_iter().collect::>(); + wi.sort_by_key(|(tid, info)| { + ( + match info.state { + WorkerState::Busy | WorkerState::Throttled(_) => 0, + WorkerState::Idle => 1, + WorkerState::Done => 2, + }, + *tid, + ) + }); + + let mut table = vec![]; + for (tid, info) in wi.iter() { + if wlo.busy && !matches!(info.state, WorkerState::Busy | WorkerState::Throttled(_)) { + continue; + } + if wlo.errors && info.errors == 0 { + continue; + } + + table.push(format!("{}\t{}\t{}", tid, info.state, info.name)); + if let Some(i) = &info.info { + table.push(format!("\t\t {}", i)); + } + let tf = timeago::Formatter::new(); + let (err_ago, err_msg) = info + .last_error + .as_ref() + .map(|(m, t)| { + ( + tf.convert(Duration::from_millis(now_msec() - t)), + m.as_str(), + ) + }) + .unwrap_or(("(?) ago".into(), "(?)")); + if info.consecutive_errors > 0 { + table.push(format!( + "\t\t {} consecutive errors ({} total), last {}", + info.consecutive_errors, info.errors, err_ago, + )); + table.push(format!("\t\t {}", err_msg)); + } else if info.errors > 0 { + table.push(format!("\t\t ({} errors, last {})", info.errors, err_ago,)); + if wlo.errors { + table.push(format!("\t\t {}", err_msg)); + } + } + } + format_table(table); +} diff --git a/src/garage/repair/online.rs b/src/garage/repair/online.rs index d6a71742..e33cf097 100644 --- a/src/garage/repair/online.rs +++ b/src/garage/repair/online.rs @@ -1,89 +1,110 @@ use std::sync::Arc; +use std::time::Duration; +use async_trait::async_trait; use tokio::sync::watch; +use garage_block::repair::ScrubWorkerCommand; use garage_model::garage::Garage; use garage_model::s3::block_ref_table::*; use garage_model::s3::object_table::*; use garage_model::s3::version_table::*; use garage_table::*; +use garage_util::background::*; use garage_util::error::Error; use crate::*; -pub struct OnlineRepair { - pub garage: Arc, +pub async fn launch_online_repair(garage: Arc, opt: RepairOpt) { + match opt.what { + RepairWhat::Tables => { + info!("Launching a full sync of tables"); + garage.bucket_table.syncer.add_full_sync(); + garage.object_table.syncer.add_full_sync(); + garage.version_table.syncer.add_full_sync(); + garage.block_ref_table.syncer.add_full_sync(); + garage.key_table.syncer.add_full_sync(); + } + RepairWhat::Versions => { + info!("Repairing the versions table"); + garage + .background + .spawn_worker(RepairVersionsWorker::new(garage.clone())); + } + RepairWhat::BlockRefs => { + info!("Repairing the block refs table"); + garage + .background + .spawn_worker(RepairBlockrefsWorker::new(garage.clone())); + } + RepairWhat::Blocks => { + info!("Repairing the stored blocks"); + garage + .background + .spawn_worker(garage_block::repair::RepairWorker::new( + garage.block_manager.clone(), + )); + } + RepairWhat::Scrub { cmd } => { + let cmd = match cmd { + ScrubCmd::Start => ScrubWorkerCommand::Start, + ScrubCmd::Pause => ScrubWorkerCommand::Pause(Duration::from_secs(3600 * 24)), + ScrubCmd::Resume => ScrubWorkerCommand::Resume, + ScrubCmd::Cancel => ScrubWorkerCommand::Cancel, + ScrubCmd::SetTranquility { tranquility } => { + ScrubWorkerCommand::SetTranquility(tranquility) + } + }; + info!("Sending command to scrub worker: {:?}", cmd); + garage.block_manager.send_scrub_command(cmd).await; + } + } } -impl OnlineRepair { - pub async fn repair_worker(&self, opt: RepairOpt, must_exit: watch::Receiver) { - if let Err(e) = self.repair_worker_aux(opt, must_exit).await { - warn!("Repair worker failed with error: {}", e); +// ---- + +struct RepairVersionsWorker { + garage: Arc, + pos: Vec, + counter: usize, +} + +impl RepairVersionsWorker { + fn new(garage: Arc) -> Self { + Self { + garage, + pos: vec![], + counter: 0, } } +} - async fn repair_worker_aux( - &self, - opt: RepairOpt, - must_exit: watch::Receiver, - ) -> Result<(), Error> { - match opt.what { - RepairWhat::Tables => { - info!("Launching a full sync of tables"); - self.garage.bucket_table.syncer.add_full_sync(); - self.garage.object_table.syncer.add_full_sync(); - self.garage.version_table.syncer.add_full_sync(); - self.garage.block_ref_table.syncer.add_full_sync(); - self.garage.key_table.syncer.add_full_sync(); - } - RepairWhat::Versions => { - info!("Repairing the versions table"); - self.repair_versions(&must_exit).await?; - } - RepairWhat::BlockRefs => { - info!("Repairing the block refs table"); - self.repair_block_ref(&must_exit).await?; - } - RepairWhat::Blocks => { - info!("Repairing the stored blocks"); - self.garage - .block_manager - .repair_data_store(&must_exit) - .await?; - } - RepairWhat::Scrub { tranquility } => { - info!("Verifying integrity of stored blocks"); - self.garage - .block_manager - .scrub_data_store(&must_exit, tranquility) - .await?; - } - } - Ok(()) +#[async_trait] +impl Worker for RepairVersionsWorker { + fn name(&self) -> String { + "Version repair worker".into() } - async fn repair_versions(&self, must_exit: &watch::Receiver) -> Result<(), Error> { - let mut pos = vec![]; - let mut i = 0; + fn info(&self) -> Option { + Some(format!("{} items done", self.counter)) + } - while !*must_exit.borrow() { - let item_bytes = match self.garage.version_table.data.store.get_gt(pos)? { - Some((k, v)) => { - pos = k; - v - } - None => break, - }; - - i += 1; - if i % 1000 == 0 { - info!("repair_versions: {}", i); + async fn work(&mut self, _must_exit: &mut watch::Receiver) -> Result { + let item_bytes = match self.garage.version_table.data.store.get_gt(&self.pos)? { + Some((k, v)) => { + self.pos = k; + v } - - let version = rmp_serde::decode::from_read_ref::<_, Version>(&item_bytes)?; - if version.deleted.get() { - continue; + None => { + info!("repair_versions: finished, done {}", self.counter); + return Ok(WorkerState::Done); } + }; + + self.counter += 1; + + let version = rmp_serde::decode::from_read_ref::<_, Version>(&item_bytes)?; + if !version.deleted.get() { let object = self .garage .object_table @@ -109,32 +130,59 @@ impl OnlineRepair { .await?; } } - info!("repair_versions: finished, done {}", i); - Ok(()) + + Ok(WorkerState::Busy) } - async fn repair_block_ref(&self, must_exit: &watch::Receiver) -> Result<(), Error> { - let mut pos = vec![]; - let mut i = 0; + async fn wait_for_work(&mut self, _must_exit: &watch::Receiver) -> WorkerState { + unreachable!() + } +} - while !*must_exit.borrow() { - let item_bytes = match self.garage.block_ref_table.data.store.get_gt(pos)? { - Some((k, v)) => { - pos = k; - v - } - None => break, - }; +// ---- - i += 1; - if i % 1000 == 0 { - info!("repair_block_ref: {}", i); +struct RepairBlockrefsWorker { + garage: Arc, + pos: Vec, + counter: usize, +} + +impl RepairBlockrefsWorker { + fn new(garage: Arc) -> Self { + Self { + garage, + pos: vec![], + counter: 0, + } + } +} + +#[async_trait] +impl Worker for RepairBlockrefsWorker { + fn name(&self) -> String { + "Block refs repair worker".into() + } + + fn info(&self) -> Option { + Some(format!("{} items done", self.counter)) + } + + async fn work(&mut self, _must_exit: &mut watch::Receiver) -> Result { + let item_bytes = match self.garage.block_ref_table.data.store.get_gt(&self.pos)? { + Some((k, v)) => { + self.pos = k; + v } - - let block_ref = rmp_serde::decode::from_read_ref::<_, BlockRef>(&item_bytes)?; - if block_ref.deleted.get() { - continue; + None => { + info!("repair_block_ref: finished, done {}", self.counter); + return Ok(WorkerState::Done); } + }; + + self.counter += 1; + + let block_ref = rmp_serde::decode::from_read_ref::<_, BlockRef>(&item_bytes)?; + if !block_ref.deleted.get() { let version = self .garage .version_table @@ -157,7 +205,11 @@ impl OnlineRepair { .await?; } } - info!("repair_block_ref: finished, done {}", i); - Ok(()) + + Ok(WorkerState::Busy) + } + + async fn wait_for_work(&mut self, _must_exit: &watch::Receiver) -> WorkerState { + unreachable!() } } diff --git a/src/model/index_counter.rs b/src/model/index_counter.rs index 36e8172b..26833390 100644 --- a/src/model/index_counter.rs +++ b/src/model/index_counter.rs @@ -2,8 +2,8 @@ use core::ops::Bound; use std::collections::{hash_map, BTreeMap, HashMap}; use std::marker::PhantomData; use std::sync::Arc; -use std::time::Duration; +use async_trait::async_trait; use serde::{Deserialize, Serialize}; use tokio::sync::{mpsc, watch}; @@ -11,6 +11,7 @@ use garage_db as db; use garage_rpc::ring::Ring; use garage_rpc::system::System; +use garage_util::background::*; use garage_util::data::*; use garage_util::error::*; use garage_util::time::*; @@ -171,11 +172,13 @@ impl IndexCounter { ), }); - let this2 = this.clone(); - background.spawn_worker( - format!("{} index counter propagator", T::COUNTER_TABLE_NAME), - move |must_exit| this2.clone().propagate_loop(propagate_rx, must_exit), - ); + background.spawn_worker(IndexPropagatorWorker { + index_counter: this.clone(), + propagate_rx, + buf: HashMap::new(), + errors: 0, + }); + this } @@ -239,68 +242,6 @@ impl IndexCounter { Ok(()) } - async fn propagate_loop( - self: Arc, - mut propagate_rx: mpsc::UnboundedReceiver<(T::CP, T::CS, LocalCounterEntry)>, - must_exit: watch::Receiver, - ) { - // This loop batches updates to counters to be sent all at once. - // They are sent once the propagate_rx channel has been emptied (or is closed). - let mut buf = HashMap::new(); - let mut errors = 0; - - loop { - let (ent, closed) = match propagate_rx.try_recv() { - Ok(ent) => (Some(ent), false), - Err(mpsc::error::TryRecvError::Empty) if buf.is_empty() => { - match propagate_rx.recv().await { - Some(ent) => (Some(ent), false), - None => (None, true), - } - } - Err(mpsc::error::TryRecvError::Empty) => (None, false), - Err(mpsc::error::TryRecvError::Disconnected) => (None, true), - }; - - if let Some((pk, sk, counters)) = ent { - let tree_key = self.table.data.tree_key(&pk, &sk); - let dist_entry = counters.into_counter_entry(self.this_node); - match buf.entry(tree_key) { - hash_map::Entry::Vacant(e) => { - e.insert(dist_entry); - } - hash_map::Entry::Occupied(mut e) => { - e.get_mut().merge(&dist_entry); - } - } - // As long as we can add entries, loop back and add them to batch - // before sending batch to other nodes - continue; - } - - if !buf.is_empty() { - let entries = buf.iter().map(|(_k, v)| v); - if let Err(e) = self.table.insert_many(entries).await { - errors += 1; - if errors >= 2 && *must_exit.borrow() { - error!("({}) Could not propagate {} counter values: {}, these counters will not be updated correctly.", T::COUNTER_TABLE_NAME, buf.len(), e); - break; - } - warn!("({}) Could not propagate {} counter values: {}, retrying in 5 seconds (retry #{})", T::COUNTER_TABLE_NAME, buf.len(), e, errors); - tokio::time::sleep(Duration::from_secs(5)).await; - continue; - } - - buf.clear(); - errors = 0; - } - - if closed || *must_exit.borrow() { - break; - } - } - } - pub fn offline_recount_all( &self, counted_table: &Arc>, @@ -437,6 +378,98 @@ impl IndexCounter { } } +struct IndexPropagatorWorker { + index_counter: Arc>, + propagate_rx: mpsc::UnboundedReceiver<(T::CP, T::CS, LocalCounterEntry)>, + + buf: HashMap, CounterEntry>, + errors: usize, +} + +impl IndexPropagatorWorker { + fn add_ent(&mut self, pk: T::CP, sk: T::CS, counters: LocalCounterEntry) { + let tree_key = self.index_counter.table.data.tree_key(&pk, &sk); + let dist_entry = counters.into_counter_entry(self.index_counter.this_node); + match self.buf.entry(tree_key) { + hash_map::Entry::Vacant(e) => { + e.insert(dist_entry); + } + hash_map::Entry::Occupied(mut e) => { + e.get_mut().merge(&dist_entry); + } + } + } +} + +#[async_trait] +impl Worker for IndexPropagatorWorker { + fn name(&self) -> String { + format!("{} index counter propagator", T::COUNTER_TABLE_NAME) + } + + fn info(&self) -> Option { + if !self.buf.is_empty() { + Some(format!("{} items in queue", self.buf.len())) + } else { + None + } + } + + async fn work(&mut self, must_exit: &mut watch::Receiver) -> Result { + // This loop batches updates to counters to be sent all at once. + // They are sent once the propagate_rx channel has been emptied (or is closed). + let closed = loop { + match self.propagate_rx.try_recv() { + Ok((pk, sk, counters)) => { + self.add_ent(pk, sk, counters); + } + Err(mpsc::error::TryRecvError::Empty) => break false, + Err(mpsc::error::TryRecvError::Disconnected) => break true, + } + }; + + if !self.buf.is_empty() { + let entries_k = self.buf.keys().take(100).cloned().collect::>(); + let entries = entries_k.iter().map(|k| self.buf.get(k).unwrap()); + if let Err(e) = self.index_counter.table.insert_many(entries).await { + self.errors += 1; + if self.errors >= 2 && *must_exit.borrow() { + error!("({}) Could not propagate {} counter values: {}, these counters will not be updated correctly.", T::COUNTER_TABLE_NAME, self.buf.len(), e); + return Ok(WorkerState::Done); + } + // Propagate error up to worker manager, it will log it, increment a counter, + // and sleep for a certain delay (with exponential backoff), waiting for + // things to go back to normal + return Err(e); + } else { + for k in entries_k { + self.buf.remove(&k); + } + self.errors = 0; + } + + return Ok(WorkerState::Busy); + } else if closed { + return Ok(WorkerState::Done); + } else { + return Ok(WorkerState::Idle); + } + } + + async fn wait_for_work(&mut self, _must_exit: &watch::Receiver) -> WorkerState { + match self.propagate_rx.recv().await { + Some((pk, sk, counters)) => { + self.add_ent(pk, sk, counters); + WorkerState::Busy + } + None => match self.buf.is_empty() { + false => WorkerState::Busy, + true => WorkerState::Done, + }, + } + } +} + #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] struct LocalCounterEntry { pk: T::CP, diff --git a/src/rpc/system.rs b/src/rpc/system.rs index 1d7c3ea4..f9f2970b 100644 --- a/src/rpc/system.rs +++ b/src/rpc/system.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::io::{Read, Write}; use std::net::{IpAddr, SocketAddr}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock}; use std::time::{Duration, Instant}; @@ -104,6 +104,9 @@ pub struct System { /// The job runner of this node pub background: Arc, + + /// Path to metadata directory + pub metadata_dir: PathBuf, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -295,6 +298,7 @@ impl System { ring, update_ring: Mutex::new(update_ring), background, + metadata_dir: config.metadata_dir.clone(), }); sys.system_endpoint.set_handler(sys.clone()); sys diff --git a/src/table/gc.rs b/src/table/gc.rs index e7fbbcb0..12218d97 100644 --- a/src/table/gc.rs +++ b/src/table/gc.rs @@ -8,12 +8,11 @@ use serde::{Deserialize, Serialize}; use serde_bytes::ByteBuf; use futures::future::join_all; -use futures::select; -use futures_util::future::*; use tokio::sync::watch; use garage_db::counted_tree_hack::CountedTree; +use garage_util::background::*; use garage_util::data::*; use garage_util::error::*; use garage_util::time::*; @@ -69,35 +68,11 @@ where gc.endpoint.set_handler(gc.clone()); - let gc1 = gc.clone(); - system.background.spawn_worker( - format!("GC loop for {}", F::TABLE_NAME), - move |must_exit: watch::Receiver| gc1.gc_loop(must_exit), - ); + system.background.spawn_worker(GcWorker::new(gc.clone())); gc } - async fn gc_loop(self: Arc, mut must_exit: watch::Receiver) { - while !*must_exit.borrow() { - match self.gc_loop_iter().await { - Ok(None) => { - // Stuff was done, loop immediately - } - Ok(Some(wait_delay)) => { - // Nothing was done, wait specified delay. - select! { - _ = tokio::time::sleep(wait_delay).fuse() => {}, - _ = must_exit.changed().fuse() => {}, - } - } - Err(e) => { - warn!("({}) Error doing GC: {}", F::TABLE_NAME, e); - } - } - } - } - async fn gc_loop_iter(&self) -> Result, Error> { let now = now_msec(); @@ -328,6 +303,66 @@ where } } +struct GcWorker +where + F: TableSchema + 'static, + R: TableReplication + 'static, +{ + gc: Arc>, + wait_delay: Duration, +} + +impl GcWorker +where + F: TableSchema + 'static, + R: TableReplication + 'static, +{ + fn new(gc: Arc>) -> Self { + Self { + gc, + wait_delay: Duration::from_secs(0), + } + } +} + +#[async_trait] +impl Worker for GcWorker +where + F: TableSchema + 'static, + R: TableReplication + 'static, +{ + fn name(&self) -> String { + format!("{} GC", F::TABLE_NAME) + } + + fn info(&self) -> Option { + let l = self.gc.data.gc_todo_len().unwrap_or(0); + if l > 0 { + Some(format!("{} items in queue", l)) + } else { + None + } + } + + async fn work(&mut self, _must_exit: &mut watch::Receiver) -> Result { + match self.gc.gc_loop_iter().await? { + None => Ok(WorkerState::Busy), + Some(delay) => { + self.wait_delay = delay; + Ok(WorkerState::Idle) + } + } + } + + async fn wait_for_work(&mut self, must_exit: &watch::Receiver) -> WorkerState { + if *must_exit.borrow() { + return WorkerState::Done; + } + tokio::time::sleep(self.wait_delay).await; + WorkerState::Busy + } +} + /// An entry stored in the gc_todo Sled tree associated with the table /// Contains helper function for parsing, saving, and removing /// such entry in Sled diff --git a/src/table/merkle.rs b/src/table/merkle.rs index 7685b193..a5c29723 100644 --- a/src/table/merkle.rs +++ b/src/table/merkle.rs @@ -1,14 +1,13 @@ use std::sync::Arc; use std::time::Duration; -use futures::select; -use futures_util::future::*; +use async_trait::async_trait; use serde::{Deserialize, Serialize}; use tokio::sync::watch; use garage_db as db; -use garage_util::background::BackgroundRunner; +use garage_util::background::*; use garage_util::data::*; use garage_util::error::Error; @@ -78,43 +77,17 @@ where empty_node_hash, }); - let ret2 = ret.clone(); - background.spawn_worker( - format!("Merkle tree updater for {}", F::TABLE_NAME), - |must_exit: watch::Receiver| ret2.updater_loop(must_exit), - ); + background.spawn_worker(MerkleWorker(ret.clone())); ret } - async fn updater_loop(self: Arc, mut must_exit: watch::Receiver) { - while !*must_exit.borrow() { - match self.updater_loop_iter() { - Ok(true) => (), - Ok(false) => { - select! { - _ = self.data.merkle_todo_notify.notified().fuse() => {}, - _ = must_exit.changed().fuse() => {}, - } - } - Err(e) => { - warn!( - "({}) Error while updating Merkle tree item: {}", - F::TABLE_NAME, - e - ); - tokio::time::sleep(Duration::from_secs(10)).await; - } - } - } - } - - fn updater_loop_iter(&self) -> Result { + fn updater_loop_iter(&self) -> Result { if let Some((key, valhash)) = self.data.merkle_todo.first()? { self.update_item(&key, &valhash)?; - Ok(true) + Ok(WorkerState::Busy) } else { - Ok(false) + Ok(WorkerState::Idle) } } @@ -325,6 +298,54 @@ where } } +struct MerkleWorker(Arc>) +where + F: TableSchema + 'static, + R: TableReplication + 'static; + +#[async_trait] +impl Worker for MerkleWorker +where + F: TableSchema + 'static, + R: TableReplication + 'static, +{ + fn name(&self) -> String { + format!("{} Merkle tree updater", F::TABLE_NAME) + } + + fn info(&self) -> Option { + let l = self.0.todo_len().unwrap_or(0); + if l > 0 { + Some(format!("{} items in queue", l)) + } else { + None + } + } + + async fn work(&mut self, _must_exit: &mut watch::Receiver) -> Result { + let updater = self.0.clone(); + tokio::task::spawn_blocking(move || { + for _i in 0..100 { + let s = updater.updater_loop_iter(); + if !matches!(s, Ok(WorkerState::Busy)) { + return s; + } + } + Ok(WorkerState::Busy) + }) + .await + .unwrap() + } + + async fn wait_for_work(&mut self, must_exit: &watch::Receiver) -> WorkerState { + if *must_exit.borrow() { + return WorkerState::Done; + } + tokio::time::sleep(Duration::from_secs(10)).await; + WorkerState::Busy + } +} + impl MerkleNodeKey { fn encode(&self) -> Vec { let mut ret = Vec::with_capacity(2 + self.prefix.len()); diff --git a/src/table/sync.rs b/src/table/sync.rs index 4c83e991..b3756a5e 100644 --- a/src/table/sync.rs +++ b/src/table/sync.rs @@ -1,17 +1,17 @@ use std::collections::VecDeque; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::time::{Duration, Instant}; use async_trait::async_trait; -use futures::select; -use futures_util::future::*; use futures_util::stream::*; use opentelemetry::KeyValue; use rand::Rng; use serde::{Deserialize, Serialize}; use serde_bytes::ByteBuf; +use tokio::select; use tokio::sync::{mpsc, watch}; +use garage_util::background::*; use garage_util::data::*; use garage_util::error::Error; @@ -34,7 +34,7 @@ pub struct TableSyncer data: Arc>, merkle: Arc>, - todo: Mutex, + add_full_sync_tx: mpsc::UnboundedSender<()>, endpoint: Arc>, } @@ -52,10 +52,6 @@ impl Rpc for SyncRpc { type Response = Result; } -struct SyncTodo { - todo: Vec, -} - #[derive(Debug, Clone)] struct TodoPartition { partition: Partition, @@ -80,118 +76,40 @@ where .netapp .endpoint(format!("garage_table/sync.rs/Rpc:{}", F::TABLE_NAME)); - let todo = SyncTodo { todo: vec![] }; + let (add_full_sync_tx, add_full_sync_rx) = mpsc::unbounded_channel(); let syncer = Arc::new(Self { system: system.clone(), data, merkle, - todo: Mutex::new(todo), + add_full_sync_tx, endpoint, }); syncer.endpoint.set_handler(syncer.clone()); - let (busy_tx, busy_rx) = mpsc::unbounded_channel(); - - let s1 = syncer.clone(); - system.background.spawn_worker( - format!("table sync watcher for {}", F::TABLE_NAME), - move |must_exit: watch::Receiver| s1.watcher_task(must_exit, busy_rx), - ); - - let s2 = syncer.clone(); - system.background.spawn_worker( - format!("table syncer for {}", F::TABLE_NAME), - move |must_exit: watch::Receiver| s2.syncer_task(must_exit, busy_tx), - ); - - let s3 = syncer.clone(); - tokio::spawn(async move { - tokio::time::sleep(Duration::from_secs(20)).await; - s3.add_full_sync(); + system.background.spawn_worker(SyncWorker { + syncer: syncer.clone(), + ring_recv: system.ring.clone(), + ring: system.ring.borrow().clone(), + add_full_sync_rx, + todo: vec![], + next_full_sync: Instant::now() + Duration::from_secs(20), }); syncer } - async fn watcher_task( - self: Arc, - mut must_exit: watch::Receiver, - mut busy_rx: mpsc::UnboundedReceiver, - ) { - let mut prev_ring: Arc = self.system.ring.borrow().clone(); - let mut ring_recv: watch::Receiver> = self.system.ring.clone(); - let mut nothing_to_do_since = Some(Instant::now()); - - while !*must_exit.borrow() { - select! { - _ = ring_recv.changed().fuse() => { - let new_ring = ring_recv.borrow(); - if !Arc::ptr_eq(&new_ring, &prev_ring) { - debug!("({}) Ring changed, adding full sync to syncer todo list", F::TABLE_NAME); - self.add_full_sync(); - prev_ring = new_ring.clone(); - } - } - busy_opt = busy_rx.recv().fuse() => { - if let Some(busy) = busy_opt { - if busy { - nothing_to_do_since = None; - } else if nothing_to_do_since.is_none() { - nothing_to_do_since = Some(Instant::now()); - } - } - } - _ = must_exit.changed().fuse() => {}, - _ = tokio::time::sleep(Duration::from_secs(1)).fuse() => { - if nothing_to_do_since.map(|t| Instant::now() - t >= ANTI_ENTROPY_INTERVAL).unwrap_or(false) { - nothing_to_do_since = None; - debug!("({}) Interval passed, adding full sync to syncer todo list", F::TABLE_NAME); - self.add_full_sync(); - } - } - } - } - } - pub fn add_full_sync(&self) { - self.todo - .lock() - .unwrap() - .add_full_sync(&self.data, &self.system); - } - - async fn syncer_task( - self: Arc, - mut must_exit: watch::Receiver, - busy_tx: mpsc::UnboundedSender, - ) { - while !*must_exit.borrow() { - let task = self.todo.lock().unwrap().pop_task(); - if let Some(partition) = task { - busy_tx.send(true).unwrap(); - let res = self - .clone() - .sync_partition(&partition, &mut must_exit) - .await; - if let Err(e) = res { - warn!( - "({}) Error while syncing {:?}: {}", - F::TABLE_NAME, - partition, - e - ); - } - } else { - busy_tx.send(false).unwrap(); - tokio::time::sleep(Duration::from_secs(1)).await; - } + if self.add_full_sync_tx.send(()).is_err() { + error!("({}) Could not add full sync", F::TABLE_NAME); } } + // ---- + async fn sync_partition( - self: Arc, + self: &Arc, partition: &TodoPartition, must_exit: &mut watch::Receiver, ) -> Result<(), Error> { @@ -577,12 +495,22 @@ where } } -impl SyncTodo { - fn add_full_sync( - &mut self, - data: &TableData, - system: &System, - ) { +// -------- Sync Worker --------- + +struct SyncWorker { + syncer: Arc>, + ring_recv: watch::Receiver>, + ring: Arc, + add_full_sync_rx: mpsc::UnboundedReceiver<()>, + todo: Vec, + next_full_sync: Instant, +} + +impl SyncWorker { + fn add_full_sync(&mut self) { + let system = &self.syncer.system; + let data = &self.syncer.data; + let my_id = system.id; self.todo.clear(); @@ -623,6 +551,8 @@ impl SyncTodo { retain, }); } + + self.next_full_sync = Instant::now() + ANTI_ENTROPY_INTERVAL; } fn pop_task(&mut self) -> Option { @@ -641,6 +571,62 @@ impl SyncTodo { } } +#[async_trait] +impl Worker for SyncWorker { + fn name(&self) -> String { + format!("{} sync", F::TABLE_NAME) + } + + fn info(&self) -> Option { + let l = self.todo.len(); + if l > 0 { + Some(format!("{} partitions remaining", l)) + } else { + None + } + } + + async fn work(&mut self, must_exit: &mut watch::Receiver) -> Result { + if let Some(partition) = self.pop_task() { + self.syncer.sync_partition(&partition, must_exit).await?; + Ok(WorkerState::Busy) + } else { + Ok(WorkerState::Idle) + } + } + + async fn wait_for_work(&mut self, must_exit: &watch::Receiver) -> WorkerState { + if *must_exit.borrow() { + return WorkerState::Done; + } + select! { + s = self.add_full_sync_rx.recv() => { + if let Some(()) = s { + self.add_full_sync(); + } + }, + _ = self.ring_recv.changed() => { + let new_ring = self.ring_recv.borrow(); + if !Arc::ptr_eq(&new_ring, &self.ring) { + self.ring = new_ring.clone(); + drop(new_ring); + debug!("({}) Ring changed, adding full sync to syncer todo list", F::TABLE_NAME); + self.add_full_sync(); + } + }, + _ = tokio::time::sleep(self.next_full_sync - Instant::now()) => { + self.add_full_sync(); + } + } + match self.todo.is_empty() { + false => WorkerState::Busy, + true => WorkerState::Idle, + } + } +} + +// ---- UTIL ---- + fn hash_of(x: &T) -> Result { Ok(blake2sum(&rmp_to_vec_all_named(x)?[..])) } diff --git a/src/util/Cargo.toml b/src/util/Cargo.toml index 5d073436..57c70ffb 100644 --- a/src/util/Cargo.toml +++ b/src/util/Cargo.toml @@ -16,6 +16,7 @@ path = "lib.rs" [dependencies] garage_db = { version = "0.8.0", path = "../db" } +async-trait = "0.1" blake2 = "0.9" err-derive = "0.3" xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] } diff --git a/src/util/background.rs b/src/util/background.rs deleted file mode 100644 index d35425f5..00000000 --- a/src/util/background.rs +++ /dev/null @@ -1,160 +0,0 @@ -//! Job runner for futures and async functions -use core::future::Future; -use std::pin::Pin; -use std::sync::Arc; -use std::time::Duration; - -use futures::future::*; -use futures::select; -use futures::stream::FuturesUnordered; -use futures::StreamExt; -use tokio::sync::{mpsc, mpsc::error::TryRecvError, watch, Mutex}; - -use crate::error::Error; - -type JobOutput = Result<(), Error>; -type Job = Pin + Send>>; - -/// Job runner for futures and async functions -pub struct BackgroundRunner { - stop_signal: watch::Receiver, - queue_in: mpsc::UnboundedSender<(Job, bool)>, - worker_in: mpsc::UnboundedSender>, -} - -impl BackgroundRunner { - /// Create a new BackgroundRunner - pub fn new( - n_runners: usize, - stop_signal: watch::Receiver, - ) -> (Arc, tokio::task::JoinHandle<()>) { - let (worker_in, mut worker_out) = mpsc::unbounded_channel(); - - let stop_signal_2 = stop_signal.clone(); - let await_all_done = tokio::spawn(async move { - let mut workers = FuturesUnordered::new(); - let mut shutdown_timer = 0; - loop { - let closed = match worker_out.try_recv() { - Ok(wkr) => { - workers.push(wkr); - false - } - Err(TryRecvError::Empty) => false, - Err(TryRecvError::Disconnected) => true, - }; - select! { - res = workers.next() => { - if let Some(Err(e)) = res { - error!("Worker exited with error: {}", e); - } - } - _ = tokio::time::sleep(Duration::from_secs(1)).fuse() => { - if closed || *stop_signal_2.borrow() { - shutdown_timer += 1; - if shutdown_timer >= 10 { - break; - } - } - } - } - } - }); - - let (queue_in, queue_out) = mpsc::unbounded_channel(); - let queue_out = Arc::new(Mutex::new(queue_out)); - - for i in 0..n_runners { - let queue_out = queue_out.clone(); - let stop_signal = stop_signal.clone(); - - worker_in - .send(tokio::spawn(async move { - loop { - let (job, cancellable) = { - select! { - item = wait_job(&queue_out).fuse() => match item { - // We received a task, process it - Some(x) => x, - // We received a signal that no more tasks will ever be sent - // because the sending side was dropped. Exit now. - None => break, - }, - _ = tokio::time::sleep(Duration::from_secs(5)).fuse() => { - if *stop_signal.borrow() { - // Nothing has been going on for 5 secs, and we are shutting - // down. Exit now. - break; - } else { - // Nothing is going on but we don't want to exit. - continue; - } - } - } - }; - if cancellable && *stop_signal.borrow() { - continue; - } - if let Err(e) = job.await { - error!("Job failed: {}", e) - } - } - info!("Background worker {} exiting", i); - })) - .unwrap(); - } - - let bgrunner = Arc::new(Self { - stop_signal, - queue_in, - worker_in, - }); - (bgrunner, await_all_done) - } - - /// Spawn a task to be run in background - pub fn spawn(&self, job: T) - where - T: Future + Send + 'static, - { - let boxed: Job = Box::pin(job); - self.queue_in - .send((boxed, false)) - .map_err(|_| "could not put job in queue") - .unwrap(); - } - - /// Spawn a task to be run in background. It may get discarded before running if spawned while - /// the runner is stopping - pub fn spawn_cancellable(&self, job: T) - where - T: Future + Send + 'static, - { - let boxed: Job = Box::pin(job); - self.queue_in - .send((boxed, true)) - .map_err(|_| "could not put job in queue") - .unwrap(); - } - - pub fn spawn_worker(&self, name: String, worker: F) - where - F: FnOnce(watch::Receiver) -> T + Send + 'static, - T: Future + Send + 'static, - { - let stop_signal = self.stop_signal.clone(); - let task = tokio::spawn(async move { - info!("Worker started: {}", name); - worker(stop_signal).await; - info!("Worker exited: {}", name); - }); - self.worker_in - .send(task) - .map_err(|_| "could not put job in queue") - .unwrap(); - } -} - -async fn wait_job(q: &Mutex>) -> Option<(Job, bool)> { - q.lock().await.recv().await -} diff --git a/src/util/background/job_worker.rs b/src/util/background/job_worker.rs new file mode 100644 index 00000000..2568ea11 --- /dev/null +++ b/src/util/background/job_worker.rs @@ -0,0 +1,48 @@ +//! Job worker: a generic worker that just processes incoming +//! jobs one by one + +use std::sync::Arc; + +use async_trait::async_trait; +use tokio::sync::{mpsc, Mutex}; + +use crate::background::worker::*; +use crate::background::*; + +pub(crate) struct JobWorker { + pub(crate) index: usize, + pub(crate) job_chan: Arc>>, + pub(crate) next_job: Option, +} + +#[async_trait] +impl Worker for JobWorker { + fn name(&self) -> String { + format!("Job worker #{}", self.index) + } + + async fn work(&mut self, _must_exit: &mut watch::Receiver) -> Result { + match self.next_job.take() { + None => return Ok(WorkerState::Idle), + Some(job) => { + job.await?; + Ok(WorkerState::Busy) + } + } + } + + async fn wait_for_work(&mut self, must_exit: &watch::Receiver) -> WorkerState { + loop { + match self.job_chan.lock().await.recv().await { + Some((job, cancellable)) => { + if cancellable && *must_exit.borrow() { + continue; + } + self.next_job = Some(job); + return WorkerState::Busy; + } + None => return WorkerState::Done, + } + } + } +} diff --git a/src/util/background/mod.rs b/src/util/background/mod.rs new file mode 100644 index 00000000..619f5068 --- /dev/null +++ b/src/util/background/mod.rs @@ -0,0 +1,117 @@ +//! Job runner for futures and async functions + +pub mod job_worker; +pub mod worker; + +use core::future::Future; + +use std::collections::HashMap; +use std::pin::Pin; +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; +use tokio::sync::{mpsc, watch, Mutex}; + +use crate::error::Error; +use worker::WorkerProcessor; +pub use worker::{Worker, WorkerState}; + +pub(crate) type JobOutput = Result<(), Error>; +pub(crate) type Job = Pin + Send>>; + +/// Job runner for futures and async functions +pub struct BackgroundRunner { + send_job: mpsc::UnboundedSender<(Job, bool)>, + send_worker: mpsc::UnboundedSender>, + worker_info: Arc>>, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct WorkerInfo { + pub name: String, + pub info: Option, + pub state: WorkerState, + pub errors: usize, + pub consecutive_errors: usize, + pub last_error: Option<(String, u64)>, +} + +impl BackgroundRunner { + /// Create a new BackgroundRunner + pub fn new( + n_runners: usize, + stop_signal: watch::Receiver, + ) -> (Arc, tokio::task::JoinHandle<()>) { + let (send_worker, worker_out) = mpsc::unbounded_channel::>(); + + let worker_info = Arc::new(std::sync::Mutex::new(HashMap::new())); + let mut worker_processor = + WorkerProcessor::new(worker_out, stop_signal, worker_info.clone()); + + let await_all_done = tokio::spawn(async move { + worker_processor.run().await; + }); + + let (send_job, queue_out) = mpsc::unbounded_channel(); + let queue_out = Arc::new(Mutex::new(queue_out)); + + for i in 0..n_runners { + let queue_out = queue_out.clone(); + + send_worker + .send(Box::new(job_worker::JobWorker { + index: i, + job_chan: queue_out.clone(), + next_job: None, + })) + .ok() + .unwrap(); + } + + let bgrunner = Arc::new(Self { + send_job, + send_worker, + worker_info, + }); + (bgrunner, await_all_done) + } + + pub fn get_worker_info(&self) -> HashMap { + self.worker_info.lock().unwrap().clone() + } + + /// Spawn a task to be run in background + pub fn spawn(&self, job: T) + where + T: Future + Send + 'static, + { + let boxed: Job = Box::pin(job); + self.send_job + .send((boxed, false)) + .ok() + .expect("Could not put job in queue"); + } + + /// Spawn a task to be run in background. It may get discarded before running if spawned while + /// the runner is stopping + pub fn spawn_cancellable(&self, job: T) + where + T: Future + Send + 'static, + { + let boxed: Job = Box::pin(job); + self.send_job + .send((boxed, true)) + .ok() + .expect("Could not put job in queue"); + } + + pub fn spawn_worker(&self, worker: W) + where + W: Worker + 'static, + { + self.send_worker + .send(Box::new(worker)) + .ok() + .expect("Could not put worker in queue"); + } +} diff --git a/src/util/background/worker.rs b/src/util/background/worker.rs new file mode 100644 index 00000000..7f573a07 --- /dev/null +++ b/src/util/background/worker.rs @@ -0,0 +1,261 @@ +use std::collections::HashMap; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use async_trait::async_trait; +use futures::future::*; +use futures::stream::FuturesUnordered; +use futures::StreamExt; +use serde::{Deserialize, Serialize}; +use tokio::select; +use tokio::sync::{mpsc, watch}; +use tracing::*; + +use crate::background::WorkerInfo; +use crate::error::Error; +use crate::time::now_msec; + +#[derive(PartialEq, Copy, Clone, Serialize, Deserialize, Debug)] +pub enum WorkerState { + Busy, + Throttled(f32), + Idle, + Done, +} + +impl std::fmt::Display for WorkerState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + WorkerState::Busy => write!(f, "Busy"), + WorkerState::Throttled(t) => write!(f, "Thr:{:.3}", t), + WorkerState::Idle => write!(f, "Idle"), + WorkerState::Done => write!(f, "Done"), + } + } +} + +#[async_trait] +pub trait Worker: Send { + fn name(&self) -> String; + + fn info(&self) -> Option { + None + } + + /// Work: do a basic unit of work, if one is available (otherwise, should return + /// WorkerState::Idle immediately). We will do our best to not interrupt this future in the + /// middle of processing, it will only be interrupted at the last minute when Garage is trying + /// to exit and this hasn't returned yet. This function may return an error to indicate that + /// its unit of work could not be processed due to an error: the error will be logged and + /// .work() will be called again after a short delay. + async fn work(&mut self, must_exit: &mut watch::Receiver) -> Result; + + /// Wait for work: await for some task to become available. This future can be interrupted in + /// the middle for any reason. This future doesn't have to await on must_exit.changed(), we + /// are doing it for you. Therefore it only receives a read refernce to must_exit which allows + /// it to check if we are exiting. + async fn wait_for_work(&mut self, must_exit: &watch::Receiver) -> WorkerState; +} + +pub(crate) struct WorkerProcessor { + stop_signal: watch::Receiver, + worker_chan: mpsc::UnboundedReceiver>, + worker_info: Arc>>, +} + +impl WorkerProcessor { + pub(crate) fn new( + worker_chan: mpsc::UnboundedReceiver>, + stop_signal: watch::Receiver, + worker_info: Arc>>, + ) -> Self { + Self { + stop_signal, + worker_chan, + worker_info, + } + } + + pub(crate) async fn run(&mut self) { + let mut workers = FuturesUnordered::new(); + let mut next_task_id = 1; + + while !*self.stop_signal.borrow() { + let await_next_worker = async { + if workers.is_empty() { + futures::future::pending().await + } else { + workers.next().await + } + }; + select! { + new_worker_opt = self.worker_chan.recv() => { + if let Some(new_worker) = new_worker_opt { + let task_id = next_task_id; + next_task_id += 1; + let stop_signal = self.stop_signal.clone(); + let stop_signal_worker = self.stop_signal.clone(); + let mut worker = WorkerHandler { + task_id, + stop_signal, + stop_signal_worker, + worker: new_worker, + state: WorkerState::Busy, + errors: 0, + consecutive_errors: 0, + last_error: None, + }; + workers.push(async move { + worker.step().await; + worker + }.boxed()); + } + } + worker = await_next_worker => { + if let Some(mut worker) = worker { + trace!("{} (TID {}): {:?}", worker.worker.name(), worker.task_id, worker.state); + + // Save worker info + let mut wi = self.worker_info.lock().unwrap(); + match wi.get_mut(&worker.task_id) { + Some(i) => { + i.state = worker.state; + i.info = worker.worker.info(); + i.errors = worker.errors; + i.consecutive_errors = worker.consecutive_errors; + if worker.last_error.is_some() { + i.last_error = worker.last_error.take(); + } + } + None => { + wi.insert(worker.task_id, WorkerInfo { + name: worker.worker.name(), + state: worker.state, + info: worker.worker.info(), + errors: worker.errors, + consecutive_errors: worker.consecutive_errors, + last_error: worker.last_error.take(), + }); + } + } + + if worker.state == WorkerState::Done { + info!("Worker {} (TID {}) exited", worker.worker.name(), worker.task_id); + } else { + workers.push(async move { + worker.step().await; + worker + }.boxed()); + } + } + } + _ = self.stop_signal.changed() => (), + } + } + + // We are exiting, drain everything + let drain_half_time = Instant::now() + Duration::from_secs(5); + let drain_everything = async move { + while let Some(mut worker) = workers.next().await { + if worker.state == WorkerState::Done { + info!( + "Worker {} (TID {}) exited", + worker.worker.name(), + worker.task_id + ); + } else if Instant::now() > drain_half_time { + warn!("Worker {} (TID {}) interrupted between two iterations in state {:?} (this should be fine)", worker.worker.name(), worker.task_id, worker.state); + } else { + workers.push( + async move { + worker.step().await; + worker + } + .boxed(), + ); + } + } + }; + + select! { + _ = drain_everything => { + info!("All workers exited peacefully \\o/"); + } + _ = tokio::time::sleep(Duration::from_secs(9)) => { + error!("Some workers could not exit in time, we are cancelling some things in the middle"); + } + } + } +} + +struct WorkerHandler { + task_id: usize, + stop_signal: watch::Receiver, + stop_signal_worker: watch::Receiver, + worker: Box, + state: WorkerState, + errors: usize, + consecutive_errors: usize, + last_error: Option<(String, u64)>, +} + +impl WorkerHandler { + async fn step(&mut self) { + match self.state { + WorkerState::Busy => match self.worker.work(&mut self.stop_signal).await { + Ok(s) => { + self.state = s; + self.consecutive_errors = 0; + } + Err(e) => { + error!( + "Error in worker {} (TID {}): {}", + self.worker.name(), + self.task_id, + e + ); + self.errors += 1; + self.consecutive_errors += 1; + self.last_error = Some((format!("{}", e), now_msec())); + // Sleep a bit so that error won't repeat immediately, exponential backoff + // strategy (min 1sec, max ~60sec) + self.state = WorkerState::Throttled( + (1.5f32).powf(std::cmp::min(10, self.consecutive_errors - 1) as f32), + ); + } + }, + WorkerState::Throttled(delay) => { + // Sleep for given delay and go back to busy state + if !*self.stop_signal.borrow() { + select! { + _ = tokio::time::sleep(Duration::from_secs_f32(delay)) => (), + _ = self.stop_signal.changed() => (), + } + } + self.state = WorkerState::Busy; + } + WorkerState::Idle => { + if *self.stop_signal.borrow() { + select! { + new_st = self.worker.wait_for_work(&self.stop_signal_worker) => { + self.state = new_st; + } + _ = tokio::time::sleep(Duration::from_secs(1)) => { + // stay in Idle state + } + } + } else { + select! { + new_st = self.worker.wait_for_work(&self.stop_signal_worker) => { + self.state = new_st; + } + _ = self.stop_signal.changed() => { + // stay in Idle state + } + } + } + } + WorkerState::Done => unreachable!(), + } + } +} diff --git a/src/util/lib.rs b/src/util/lib.rs index 8ca6e310..fce151af 100644 --- a/src/util/lib.rs +++ b/src/util/lib.rs @@ -11,7 +11,6 @@ pub mod error; pub mod formater; pub mod metrics; pub mod persister; -//pub mod sled_counter; pub mod time; pub mod token_bucket; pub mod tranquilizer; diff --git a/src/util/tranquilizer.rs b/src/util/tranquilizer.rs index 28711387..fdb2918b 100644 --- a/src/util/tranquilizer.rs +++ b/src/util/tranquilizer.rs @@ -3,6 +3,8 @@ use std::time::{Duration, Instant}; use tokio::time::sleep; +use crate::background::WorkerState; + /// A tranquilizer is a helper object that is used to make /// background operations not take up too much time. /// @@ -33,7 +35,7 @@ impl Tranquilizer { } } - pub async fn tranquilize(&mut self, tranquility: u32) { + fn tranquilize_internal(&mut self, tranquility: u32) -> Option { let observation = Instant::now() - self.last_step_begin; self.observations.push_back(observation); @@ -45,13 +47,32 @@ impl Tranquilizer { if !self.observations.is_empty() { let delay = (tranquility * self.sum_observations) / (self.observations.len() as u32); - sleep(delay).await; + Some(delay) + } else { + None } + } - self.reset(); + pub async fn tranquilize(&mut self, tranquility: u32) { + if let Some(delay) = self.tranquilize_internal(tranquility) { + sleep(delay).await; + self.reset(); + } + } + + #[must_use] + pub fn tranquilize_worker(&mut self, tranquility: u32) -> WorkerState { + match self.tranquilize_internal(tranquility) { + Some(delay) => WorkerState::Throttled(delay.as_secs_f32()), + None => WorkerState::Busy, + } } pub fn reset(&mut self) { self.last_step_begin = Instant::now(); } + + pub fn clear(&mut self) { + self.observations.clear(); + } } From ac03fa7937d9da29d2358343a499fe9d15ac5f7c Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 15 Jul 2022 18:31:19 +0200 Subject: [PATCH 032/149] Uniformize tracing::* imports (hopefully fixes 32-bit build) --- src/db/lib.rs | 3 +++ src/db/lmdb_adapter.rs | 1 - src/db/sqlite_adapter.rs | 2 -- src/util/background/worker.rs | 1 - 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/db/lib.rs b/src/db/lib.rs index 8188c715..f185114e 100644 --- a/src/db/lib.rs +++ b/src/db/lib.rs @@ -1,3 +1,6 @@ +#[macro_use] +extern crate tracing; + pub mod lmdb_adapter; pub mod sled_adapter; pub mod sqlite_adapter; diff --git a/src/db/lmdb_adapter.rs b/src/db/lmdb_adapter.rs index fdb254c6..c036c990 100644 --- a/src/db/lmdb_adapter.rs +++ b/src/db/lmdb_adapter.rs @@ -345,7 +345,6 @@ pub fn recommended_map_size() -> usize { #[cfg(target_pointer_width = "32")] pub fn recommended_map_size() -> usize { - use log::warn; warn!("LMDB is not recommended on 32-bit systems, database size will be limited"); 1usize << 30 } diff --git a/src/db/sqlite_adapter.rs b/src/db/sqlite_adapter.rs index 97a78b07..886fda6e 100644 --- a/src/db/sqlite_adapter.rs +++ b/src/db/sqlite_adapter.rs @@ -6,8 +6,6 @@ use std::pin::Pin; use std::ptr::NonNull; use std::sync::{Arc, Mutex, MutexGuard}; -use tracing::trace; - use rusqlite::{params, Connection, Rows, Statement, Transaction}; use crate::{ diff --git a/src/util/background/worker.rs b/src/util/background/worker.rs index 7f573a07..f5e3addb 100644 --- a/src/util/background/worker.rs +++ b/src/util/background/worker.rs @@ -9,7 +9,6 @@ use futures::StreamExt; use serde::{Deserialize, Serialize}; use tokio::select; use tokio::sync::{mpsc, watch}; -use tracing::*; use crate::background::WorkerInfo; use crate::error::Error; From 76cb34a0ae716173d9bed6d6dcb53903762255a8 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 20 Jul 2022 14:44:30 +0200 Subject: [PATCH 033/149] Fail if compiled binary is dynamic --- .drone.yml | 45 ++++++++++-------- script/not-dynamic.sh | 14 ++++++ shell.nix | 104 ++++++++++++++++++++++++------------------ 3 files changed, 98 insertions(+), 65 deletions(-) create mode 100755 script/not-dynamic.sh diff --git a/.drone.yml b/.drone.yml index 903be5b0..01adc278 100644 --- a/.drone.yml +++ b/.drone.yml @@ -25,7 +25,7 @@ steps: path: /etc/nix commands: - cp nix/nix.conf /etc/nix/nix.conf - - nix-build --no-build-output --no-out-link shell.nix --arg release false -A inputDerivation + - nix-build --no-build-output --no-out-link shell.nix -A rust.inputDerivation -A integration.inputDerivation -A release.inputDerivation - name: code quality image: nixpkgs/nix:nixos-21.05 @@ -35,8 +35,8 @@ steps: - name: nix_config path: /etc/nix commands: - - nix-shell --arg release false --run "cargo fmt -- --check" - - nix-shell --arg release false --run "cargo clippy -- --deny warnings" + - nix-shell --attr rust --run "cargo fmt -- --check" + - nix-shell --attr rust --run "cargo clippy -- --deny warnings" - name: build image: nixpkgs/nix:nixos-21.05 @@ -47,6 +47,7 @@ steps: path: /etc/nix commands: - nix-build --no-build-output --option log-lines 100 --argstr target x86_64-unknown-linux-musl --arg release false --argstr git_version $DRONE_COMMIT + - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: unit + func tests image: nixpkgs/nix:nixos-21.05 @@ -82,7 +83,7 @@ steps: path: /etc/nix commands: - nix-build --no-build-output --argstr target x86_64-unknown-linux-musl --arg release false --argstr git_version $DRONE_COMMIT - - nix-shell --arg release false --run ./script/test-smoke.sh || (cat /tmp/garage.log; false) + - nix-shell --attr integration --run ./script/test-smoke.sh || (cat /tmp/garage.log; false) trigger: event: @@ -120,7 +121,7 @@ steps: path: /etc/nix commands: - cp nix/nix.conf /etc/nix/nix.conf - - nix-build --no-build-output --no-out-link shell.nix -A inputDerivation + - nix-build --no-build-output --no-out-link shell.nix -A rust.inputDerivation -A integration.inputDerivation -A release.inputDerivation - name: build image: nixpkgs/nix:nixos-21.05 @@ -131,6 +132,7 @@ steps: path: /etc/nix commands: - nix-build --no-build-output --argstr target $TARGET --arg release true --argstr git_version $DRONE_COMMIT + - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: integration image: nixpkgs/nix:nixos-21.05 @@ -140,7 +142,7 @@ steps: - name: nix_config path: /etc/nix commands: - - nix-shell --run ./script/test-smoke.sh || (cat /tmp/garage.log; false) + - nix-shell --attr integration --run ./script/test-smoke.sh || (cat /tmp/garage.log; false) - name: push static binary image: nixpkgs/nix:nixos-21.05 @@ -155,7 +157,7 @@ steps: AWS_SECRET_ACCESS_KEY: from_secret: garagehq_aws_secret_access_key commands: - - nix-shell --arg rust false --arg integration false --run "to_s3" + - nix-shell --attr release --run "to_s3" - name: docker build and publish image: nixpkgs/nix:nixos-21.05 @@ -174,7 +176,7 @@ steps: - mkdir -p /kaniko/.docker - echo $DOCKER_AUTH > /kaniko/.docker/config.json - export CONTAINER_TAG=${DRONE_TAG:-$DRONE_COMMIT} - - nix-shell --arg rust false --arg integration false --run "to_docker" + - nix-shell --attr release --run "to_docker" trigger: @@ -210,7 +212,7 @@ steps: path: /etc/nix commands: - cp nix/nix.conf /etc/nix/nix.conf - - nix-build --no-build-output --no-out-link shell.nix -A inputDerivation + - nix-build --no-build-output --no-out-link shell.nix -A rust.inputDerivation -A integration.inputDerivation -A release.inputDerivation - name: build image: nixpkgs/nix:nixos-21.05 @@ -221,6 +223,7 @@ steps: path: /etc/nix commands: - nix-build --no-build-output --argstr target $TARGET --arg release true --argstr git_version $DRONE_COMMIT + - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: integration image: nixpkgs/nix:nixos-21.05 @@ -230,7 +233,7 @@ steps: - name: nix_config path: /etc/nix commands: - - nix-shell --run ./script/test-smoke.sh || (cat /tmp/garage.log; false) + - nix-shell --attr integration --run ./script/test-smoke.sh || (cat /tmp/garage.log; false) - name: push static binary image: nixpkgs/nix:nixos-21.05 @@ -245,7 +248,7 @@ steps: AWS_SECRET_ACCESS_KEY: from_secret: garagehq_aws_secret_access_key commands: - - nix-shell --arg rust false --arg integration false --run "to_s3" + - nix-shell --attr release --run "to_s3" - name: docker build and publish image: nixpkgs/nix:nixos-21.05 @@ -264,7 +267,7 @@ steps: - mkdir -p /kaniko/.docker - echo $DOCKER_AUTH > /kaniko/.docker/config.json - export CONTAINER_TAG=${DRONE_TAG:-$DRONE_COMMIT} - - nix-shell --arg rust false --arg integration false --run "to_docker" + - nix-shell --attr release --run "to_docker" trigger: event: @@ -299,7 +302,7 @@ steps: path: /etc/nix commands: - cp nix/nix.conf /etc/nix/nix.conf - - nix-build --no-build-output --no-out-link ./shell.nix --arg rust false --arg integration false -A inputDerivation + - nix-build --no-build-output --no-out-link shell.nix -A rust.inputDerivation -A integration.inputDerivation -A release.inputDerivation - name: build image: nixpkgs/nix:nixos-21.05 @@ -310,6 +313,7 @@ steps: path: /etc/nix commands: - nix-build --no-build-output --argstr target $TARGET --arg release true --argstr git_version $DRONE_COMMIT + - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: push static binary image: nixpkgs/nix:nixos-21.05 @@ -324,7 +328,7 @@ steps: AWS_SECRET_ACCESS_KEY: from_secret: garagehq_aws_secret_access_key commands: - - nix-shell --arg rust false --arg integration false --run "to_s3" + - nix-shell --attr release --run "to_s3" - name: docker build and publish image: nixpkgs/nix:nixos-21.05 @@ -343,7 +347,7 @@ steps: - mkdir -p /kaniko/.docker - echo $DOCKER_AUTH > /kaniko/.docker/config.json - export CONTAINER_TAG=${DRONE_TAG:-$DRONE_COMMIT} - - nix-shell --arg rust false --arg integration false --run "to_docker" + - nix-shell --attr release --run "to_docker" trigger: event: @@ -378,7 +382,7 @@ steps: path: /etc/nix commands: - cp nix/nix.conf /etc/nix/nix.conf - - nix-build --no-build-output --no-out-link --arg rust false --arg integration false -A inputDerivation + - nix-build --no-build-output --no-out-link shell.nix -A rust.inputDerivation -A integration.inputDerivation -A release.inputDerivation - name: build image: nixpkgs/nix:nixos-21.05 @@ -389,6 +393,7 @@ steps: path: /etc/nix commands: - nix-build --no-build-output --argstr target $TARGET --arg release true --argstr git_version $DRONE_COMMIT + - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: push static binary image: nixpkgs/nix:nixos-21.05 @@ -403,7 +408,7 @@ steps: AWS_SECRET_ACCESS_KEY: from_secret: garagehq_aws_secret_access_key commands: - - nix-shell --arg integration false --arg rust false --run "to_s3" + - nix-shell --attr release --run "to_s3" - name: docker build and publish image: nixpkgs/nix:nixos-21.05 @@ -422,7 +427,7 @@ steps: - mkdir -p /kaniko/.docker - echo $DOCKER_AUTH > /kaniko/.docker/config.json - export CONTAINER_TAG=${DRONE_TAG:-$DRONE_COMMIT} - - nix-shell --arg rust false --arg integration false --run "to_docker" + - nix-shell --attr release --run "to_docker" trigger: event: @@ -455,7 +460,7 @@ steps: from_secret: garagehq_aws_secret_access_key commands: - mkdir -p /etc/nix && cp nix/nix.conf /etc/nix/nix.conf - - nix-shell --arg integration false --arg rust false --run "refresh_index" + - nix-shell --attr release --run "refresh_index" depends_on: - release-linux-x86_64 @@ -473,6 +478,6 @@ node: --- kind: signature -hmac: 3fc19d6f9a3555519c8405e3281b2e74289bb802f644740d5481d53df3a01fa4 +hmac: 60fad5d78c12616be848aae35703f250300abab5f2eda08eb48fe3afd6cc58c8 ... diff --git a/script/not-dynamic.sh b/script/not-dynamic.sh new file mode 100755 index 00000000..b9a13070 --- /dev/null +++ b/script/not-dynamic.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -e + +if [ "$#" -ne 1 ]; then + echo "[fail] usage: $0 binary" + exit 2 +fi + +if file $1 | grep 'dynamically linked' 2>&1; then + echo "[fail] $1 is dynamic" + exit 1 +fi +echo "[ok] $1 is probably static" diff --git a/shell.nix b/shell.nix index 13ea4a0e..eaedb6b8 100644 --- a/shell.nix +++ b/shell.nix @@ -1,8 +1,5 @@ { system ? builtins.currentSystem, - rust ? true, - integration ? true, - release ? true, }: with import ./nix/common.nix; @@ -16,9 +13,59 @@ let winscp = (import ./nix/winscp.nix) pkgs; in + { -pkgs.mkShell { - shellHook = '' + /* --- Rust Shell --- + * Use it to compile Garage + */ + rust = pkgs.mkShell { + shellHook = '' +function refresh_toolchain { + nix copy \ + --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/etc/nix/signing-key.sec' \ + $(nix-store -qR \ + $(nix-build --quiet --no-build-output --no-out-link nix/toolchain.nix)) +} + ''; + + nativeBuildInputs = [ + pkgs.rustPlatform.rust.rustc + pkgs.rustPlatform.rust.cargo + pkgs.clippy + pkgs.rustfmt + pkgs.perl + pkgs.protobuf + pkgs.pkg-config + pkgs.openssl + pkgs.file + cargo2nix.packages.x86_64-linux.cargo2nix + ]; + }; + + /* --- Integration shell --- + * Use it to test Garage with common S3 clients + */ + integration = pkgs.mkShell { + nativeBuildInputs = [ + winscp + pkgs.s3cmd + pkgs.awscli2 + pkgs.minio-client + pkgs.rclone + pkgs.socat + pkgs.psmisc + pkgs.which + pkgs.openssl + pkgs.curl + pkgs.jq + ]; + }; + + /* --- Release shell --- + * A shell built to make releasing easier + */ + release = pkgs.mkShell { + shellHook = '' function to_s3 { aws \ --endpoint-url https://garage.deuxfleurs.fr \ @@ -62,45 +109,12 @@ function refresh_index { result/share/_releases.html \ s3://garagehq.deuxfleurs.fr/ } + ''; + nativeBuildInputs = [ + pkgs.awscli2 + kaniko + ]; + }; + } -function refresh_toolchain { - nix copy \ - --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/etc/nix/signing-key.sec' \ - $(nix-store -qR \ - $(nix-build --quiet --no-build-output --no-out-link nix/toolchain.nix)) -} - ''; - nativeBuildInputs = - (if rust then [ - pkgs.rustPlatform.rust.rustc - pkgs.rustPlatform.rust.cargo - pkgs.clippy - pkgs.rustfmt - pkgs.perl - pkgs.protobuf - pkgs.pkg-config - pkgs.openssl - cargo2nix.packages.x86_64-linux.cargo2nix - ] else []) - ++ - (if integration then [ - winscp - pkgs.s3cmd - pkgs.awscli2 - pkgs.minio-client - pkgs.rclone - pkgs.socat - pkgs.psmisc - pkgs.which - pkgs.openssl - pkgs.curl - pkgs.jq - ] else []) - ++ - (if release then [ - pkgs.awscli2 - kaniko - ] else []) - ; -} From 9c9e4833750c19b5fb04a5241df4fd77be6fff78 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Sat, 23 Jul 2022 12:02:40 +0200 Subject: [PATCH 034/149] Put log-lines in nix.conf --- .drone.yml | 2 +- nix/nix.conf | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 01adc278..34a6eda7 100644 --- a/.drone.yml +++ b/.drone.yml @@ -46,7 +46,7 @@ steps: - name: nix_config path: /etc/nix commands: - - nix-build --no-build-output --option log-lines 100 --argstr target x86_64-unknown-linux-musl --arg release false --argstr git_version $DRONE_COMMIT + - nix-build --no-build-output --argstr target x86_64-unknown-linux-musl --arg release false --argstr git_version $DRONE_COMMIT - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: unit + func tests diff --git a/nix/nix.conf b/nix/nix.conf index 871efb10..5a9de951 100644 --- a/nix/nix.conf +++ b/nix/nix.conf @@ -2,3 +2,4 @@ substituters = https://cache.nixos.org https://nix.web.deuxfleurs.fr trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= nix.web.deuxfleurs.fr:eTGL6kvaQn6cDR/F9lDYUIP9nCVR/kkshYfLDJf1yKs= max-jobs = auto cores = 4 +log-lines = 200 From a49d0ea19f000036ad39b7d6a1134bbd0d07ab1d Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Sat, 23 Jul 2022 12:17:41 +0200 Subject: [PATCH 035/149] Fix: compile aarch64+armv6 as static binaries --- default.nix | 56 +++++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/default.nix b/default.nix index de996ac1..296c7592 100644 --- a/default.nix +++ b/default.nix @@ -56,45 +56,47 @@ in let */ overrides = pkgs.rustBuilder.overrides.all ++ [ /* - [1] We need to alter Nix hardening to be able to statically compile: PIE, + [1] We need to alter Nix hardening to make static binaries: PIE, Position Independent Executables seems to be supported only on amd64. Having - this flags set either make our executables crash or compile as dynamic on many platforms. - In the following section codegenOpts, we reactive it for the supported targets - (only amd64 curently) through the `-static-pie` flag. PIE is a feature used - by ASLR, which helps mitigate security issues. - Learn more about Nix Hardening: https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/cc-wrapper/add-hardening.sh - - [2] We want to inject the git version while keeping the build deterministic. - As we do not want to consider the .git folder as part of the input source, - we ask the user (the CI often) to pass the value to Nix. + this flag set either 1. make our executables crash or 2. compile as dynamic on some platforms. + Here, we deactivate it. Later (find `codegenOpts`), we reactivate it for supported targets + (only amd64 curently) through the `-static-pie` flag. + PIE is a feature used by ASLR, which helps mitigate security issues. + Learn more about Nix Hardening at: https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/cc-wrapper/add-hardening.sh */ + (pkgs.rustBuilder.rustLib.makeOverride { + name = "garage"; + overrideAttrs = drv: { hardeningDisable = [ "pie" ]; }; + }) + (pkgs.rustBuilder.rustLib.makeOverride { name = "garage_rpc"; + + /* + [2] We want to inject the git version while keeping the build deterministic. + As we do not want to consider the .git folder as part of the input source, + we ask the user (the CI often) to pass the value to Nix. + */ overrideAttrs = drv: - /* [1] */ { hardeningDisable = [ "pie" ]; } - // - /* [2] */ (if git_version != null then { + (if git_version != null then { preConfigure = '' ${drv.preConfigure or ""} export GIT_VERSION="${git_version}" ''; } else {}); + + /* + [3] We ship some parts of the code disabled by default by putting them behind a flag. + It speeds up the compilation (when the feature is not required) and released crates have less dependency by default (less attack surface, disk space, etc.). + But we want to ship these additional features when we release Garage. + In the end, we chose to exclude all features from debug builds while putting (all of) them in the release builds. + Currently, the only feature of Garage is kubernetes-discovery from the garage_rpc crate. + */ + overrideArgs = old: { + features = if release then [ "kubernetes-discovery" ] else []; + }; }) - /* - We ship some parts of the code disabled by default by putting them behind a flag. - It speeds up the compilation (when the feature is not required) and released crates have less dependency by default (less attack surface, disk space, etc.). - But we want to ship these additional features when we release Garage. - In the end, we chose to exclude all features from debug builds while putting (all of) them in the release builds. - Currently, the only feature of Garage is kubernetes-discovery from the garage_rpc crate. - */ - (pkgs.rustBuilder.rustLib.makeOverride { - name = "garage_rpc"; - overrideArgs = old: - { - features = if release then [ "kubernetes-discovery" ] else []; - }; - }) ]; packageFun = import ./Cargo.nix; From 96561c48a154a7617cf2766bdee70d581be67b93 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Sun, 24 Jul 2022 11:08:02 +0200 Subject: [PATCH 036/149] Bump Nix image to 22.05 --- .drone.yml | 83 ++++++++++++++++++++++++---------------------------- nix/nix.conf | 2 ++ 2 files changed, 40 insertions(+), 45 deletions(-) diff --git a/.drone.yml b/.drone.yml index 34a6eda7..726387a4 100644 --- a/.drone.yml +++ b/.drone.yml @@ -16,19 +16,30 @@ environment: HOME: /drone/garage steps: - - name: setup nix - image: nixpkgs/nix:nixos-21.05 + - name: nix maintainance + image: nixpkgs/nix:nixos-22.05 + volumes: + - name: nix_store + path: /mnt + - name: nix_config + path: /etc/nix + commands: + - "[ -d /mnt/store/3vpyn2qz5ay057nq9x68sh0r328d77ng-nix-2.8.1/ ] || (mkdir -p /mnt/store && cp -r /nix/store/* /mnt/store/)" + - "[ -d /mnt/var/ ] || cp -r /nix/var /mnt/" + - cp nix/nix.conf /etc/nix/nix.conf + + - name: warmup cache + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix - name: nix_config path: /etc/nix commands: - - cp nix/nix.conf /etc/nix/nix.conf - nix-build --no-build-output --no-out-link shell.nix -A rust.inputDerivation -A integration.inputDerivation -A release.inputDerivation - name: code quality - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -39,7 +50,7 @@ steps: - nix-shell --attr rust --run "cargo clippy -- --deny warnings" - name: build - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -50,7 +61,7 @@ steps: - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: unit + func tests - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 environment: GARAGE_TEST_INTEGRATION_EXE: result/bin/garage volumes: @@ -75,7 +86,7 @@ steps: - ./result/bin/integration-* - name: smoke-test - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -93,9 +104,6 @@ trigger: - tag - cron -node: - nix: 1 - --- kind: pipeline type: docker @@ -113,7 +121,7 @@ environment: steps: - name: setup nix - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -124,7 +132,7 @@ steps: - nix-build --no-build-output --no-out-link shell.nix -A rust.inputDerivation -A integration.inputDerivation -A release.inputDerivation - name: build - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -135,7 +143,7 @@ steps: - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: integration - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -145,7 +153,7 @@ steps: - nix-shell --attr integration --run ./script/test-smoke.sh || (cat /tmp/garage.log; false) - name: push static binary - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -160,7 +168,7 @@ steps: - nix-shell --attr release --run "to_s3" - name: docker build and publish - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -184,9 +192,6 @@ trigger: - promote - cron -node: - nix: 1 - --- kind: pipeline type: docker @@ -204,7 +209,7 @@ environment: steps: - name: setup nix - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -215,7 +220,7 @@ steps: - nix-build --no-build-output --no-out-link shell.nix -A rust.inputDerivation -A integration.inputDerivation -A release.inputDerivation - name: build - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -226,7 +231,7 @@ steps: - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: integration - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -236,7 +241,7 @@ steps: - nix-shell --attr integration --run ./script/test-smoke.sh || (cat /tmp/garage.log; false) - name: push static binary - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -251,7 +256,7 @@ steps: - nix-shell --attr release --run "to_s3" - name: docker build and publish - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -274,9 +279,6 @@ trigger: - promote - cron -node: - nix: 1 - --- kind: pipeline type: docker @@ -294,7 +296,7 @@ environment: steps: - name: setup nix - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -305,7 +307,7 @@ steps: - nix-build --no-build-output --no-out-link shell.nix -A rust.inputDerivation -A integration.inputDerivation -A release.inputDerivation - name: build - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -316,7 +318,7 @@ steps: - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: push static binary - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -331,7 +333,7 @@ steps: - nix-shell --attr release --run "to_s3" - name: docker build and publish - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -354,9 +356,6 @@ trigger: - promote - cron -node: - nix: 1 - --- kind: pipeline type: docker @@ -374,7 +373,7 @@ environment: steps: - name: setup nix - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -385,7 +384,7 @@ steps: - nix-build --no-build-output --no-out-link shell.nix -A rust.inputDerivation -A integration.inputDerivation -A release.inputDerivation - name: build - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -396,7 +395,7 @@ steps: - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: push static binary - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -411,7 +410,7 @@ steps: - nix-shell --attr release --run "to_s3" - name: docker build and publish - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -434,9 +433,6 @@ trigger: - promote - cron -node: - nix: 1 - --- kind: pipeline type: docker @@ -449,7 +445,7 @@ volumes: steps: - name: refresh-index - image: nixpkgs/nix:nixos-21.05 + image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store path: /nix @@ -473,11 +469,8 @@ trigger: - promote - cron -node: - nix: 1 - --- kind: signature -hmac: 60fad5d78c12616be848aae35703f250300abab5f2eda08eb48fe3afd6cc58c8 +hmac: 12b06094741a9b6da448e3a176d2fc37b2c261ab87acefa60a070e67a55352b0 ... diff --git a/nix/nix.conf b/nix/nix.conf index 5a9de951..f3defe69 100644 --- a/nix/nix.conf +++ b/nix/nix.conf @@ -3,3 +3,5 @@ trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDS max-jobs = auto cores = 4 log-lines = 200 +filter-syscalls = false +sandbox = false From 5fb858424793de8dc35dce8deaa8981a384e064f Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Mon, 25 Jul 2022 14:58:47 +0200 Subject: [PATCH 037/149] Refactor default.nix to follow Nix Flakes patterns --- .drone.yml | 87 +++++++++++--------------- default.nix | 159 +++++++----------------------------------------- nix/compile.nix | 140 ++++++++++++++++++++++++++++++++++++++++++ nix/nix.conf | 2 + 4 files changed, 200 insertions(+), 188 deletions(-) create mode 100644 nix/compile.nix diff --git a/.drone.yml b/.drone.yml index 726387a4..f76e162d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -36,7 +36,7 @@ steps: - name: nix_config path: /etc/nix commands: - - nix-build --no-build-output --no-out-link shell.nix -A rust.inputDerivation -A integration.inputDerivation -A release.inputDerivation + - nix-build --no-build-output --no-out-link shell.nix -A rust.inputDerivation -A integration.inputDerivation - name: code quality image: nixpkgs/nix:nixos-22.05 @@ -57,7 +57,7 @@ steps: - name: nix_config path: /etc/nix commands: - - nix-build --no-build-output --argstr target x86_64-unknown-linux-musl --arg release false --argstr git_version $DRONE_COMMIT + - nix-build --no-build-output --attr pkgs.amd64.debug --argstr git_version $DRONE_COMMIT - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: unit + func tests @@ -70,12 +70,7 @@ steps: - name: nix_config path: /etc/nix commands: - - | - nix-build \ - --no-build-output \ - --option log-lines 100 \ - --argstr target x86_64-unknown-linux-musl \ - --argstr compileMode test + - nix-build --no-build-output --attr test.amd64 - ./result/bin/garage_api-* - ./result/bin/garage_model-* - ./result/bin/garage_rpc-* @@ -93,7 +88,7 @@ steps: - name: nix_config path: /etc/nix commands: - - nix-build --no-build-output --argstr target x86_64-unknown-linux-musl --arg release false --argstr git_version $DRONE_COMMIT + - nix-build --no-build-output --attr pkgs.amd64.debug --argstr git_version $DRONE_COMMIT - nix-shell --attr integration --run ./script/test-smoke.sh || (cat /tmp/garage.log; false) trigger: @@ -107,7 +102,7 @@ trigger: --- kind: pipeline type: docker -name: release-linux-x86_64 +name: release-linux-amd64 volumes: - name: nix_store @@ -116,20 +111,18 @@ volumes: - name: nix_config temp: {} -environment: - TARGET: x86_64-unknown-linux-musl - steps: - - name: setup nix + - name: nix maintainance image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store - path: /nix + path: /mnt - name: nix_config path: /etc/nix commands: - - cp nix/nix.conf /etc/nix/nix.conf - - nix-build --no-build-output --no-out-link shell.nix -A rust.inputDerivation -A integration.inputDerivation -A release.inputDerivation + - "[ -d /mnt/store/3vpyn2qz5ay057nq9x68sh0r328d77ng-nix-2.8.1/ ] || (mkdir -p /mnt/store && cp -r /nix/store/* /mnt/store/)" + - "[ -d /mnt/var/ ] || cp -r /nix/var /mnt/" + - cp nix/nix.conf /etc/nix/nix.conf - name: build image: nixpkgs/nix:nixos-22.05 @@ -139,7 +132,7 @@ steps: - name: nix_config path: /etc/nix commands: - - nix-build --no-build-output --argstr target $TARGET --arg release true --argstr git_version $DRONE_COMMIT + - nix-build --no-build-output --attr pkgs.amd64.release --argstr git_version $DRONE_COMMIT - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: integration @@ -195,7 +188,7 @@ trigger: --- kind: pipeline type: docker -name: release-linux-i686 +name: release-linux-i386 volumes: - name: nix_store @@ -204,20 +197,18 @@ volumes: - name: nix_config temp: {} -environment: - TARGET: i686-unknown-linux-musl - steps: - - name: setup nix + - name: nix maintainance image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store - path: /nix + path: /mnt - name: nix_config path: /etc/nix commands: - - cp nix/nix.conf /etc/nix/nix.conf - - nix-build --no-build-output --no-out-link shell.nix -A rust.inputDerivation -A integration.inputDerivation -A release.inputDerivation + - "[ -d /mnt/store/3vpyn2qz5ay057nq9x68sh0r328d77ng-nix-2.8.1/ ] || (mkdir -p /mnt/store && cp -r /nix/store/* /mnt/store/)" + - "[ -d /mnt/var/ ] || cp -r /nix/var /mnt/" + - cp nix/nix.conf /etc/nix/nix.conf - name: build image: nixpkgs/nix:nixos-22.05 @@ -227,7 +218,7 @@ steps: - name: nix_config path: /etc/nix commands: - - nix-build --no-build-output --argstr target $TARGET --arg release true --argstr git_version $DRONE_COMMIT + - nix-build --no-build-output --attr pkgs.i386.release --argstr git_version $DRONE_COMMIT - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: integration @@ -282,7 +273,7 @@ trigger: --- kind: pipeline type: docker -name: release-linux-aarch64 +name: release-linux-arm64 volumes: - name: nix_store @@ -291,20 +282,18 @@ volumes: - name: nix_config temp: {} -environment: - TARGET: aarch64-unknown-linux-musl - steps: - - name: setup nix + - name: nix maintainance image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store - path: /nix + path: /mnt - name: nix_config path: /etc/nix commands: - - cp nix/nix.conf /etc/nix/nix.conf - - nix-build --no-build-output --no-out-link shell.nix -A rust.inputDerivation -A integration.inputDerivation -A release.inputDerivation + - "[ -d /mnt/store/3vpyn2qz5ay057nq9x68sh0r328d77ng-nix-2.8.1/ ] || (mkdir -p /mnt/store && cp -r /nix/store/* /mnt/store/)" + - "[ -d /mnt/var/ ] || cp -r /nix/var /mnt/" + - cp nix/nix.conf /etc/nix/nix.conf - name: build image: nixpkgs/nix:nixos-22.05 @@ -314,7 +303,7 @@ steps: - name: nix_config path: /etc/nix commands: - - nix-build --no-build-output --argstr target $TARGET --arg release true --argstr git_version $DRONE_COMMIT + - nix-build --no-build-output --attr pkgs.arm64.release --argstr git_version $DRONE_COMMIT - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: push static binary @@ -359,7 +348,7 @@ trigger: --- kind: pipeline type: docker -name: release-linux-armv6l +name: release-linux-arm volumes: - name: nix_store @@ -368,20 +357,18 @@ volumes: - name: nix_config temp: {} -environment: - TARGET: armv6l-unknown-linux-musleabihf - steps: - - name: setup nix + - name: nix maintainance image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store - path: /nix + path: /mnt - name: nix_config path: /etc/nix commands: - - cp nix/nix.conf /etc/nix/nix.conf - - nix-build --no-build-output --no-out-link shell.nix -A rust.inputDerivation -A integration.inputDerivation -A release.inputDerivation + - "[ -d /mnt/store/3vpyn2qz5ay057nq9x68sh0r328d77ng-nix-2.8.1/ ] || (mkdir -p /mnt/store && cp -r /nix/store/* /mnt/store/)" + - "[ -d /mnt/var/ ] || cp -r /nix/var /mnt/" + - cp nix/nix.conf /etc/nix/nix.conf - name: build image: nixpkgs/nix:nixos-22.05 @@ -391,7 +378,7 @@ steps: - name: nix_config path: /etc/nix commands: - - nix-build --no-build-output --argstr target $TARGET --arg release true --argstr git_version $DRONE_COMMIT + - nix-build --no-build-output --attr pkgs.arm.release --argstr git_version $DRONE_COMMIT - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: push static binary @@ -459,10 +446,10 @@ steps: - nix-shell --attr release --run "refresh_index" depends_on: - - release-linux-x86_64 - - release-linux-i686 - - release-linux-aarch64 - - release-linux-armv6l + - release-linux-amd64 + - release-linux-i386 + - release-linux-arm64 + - release-linux-arm trigger: event: @@ -471,6 +458,6 @@ trigger: --- kind: signature -hmac: 12b06094741a9b6da448e3a176d2fc37b2c261ab87acefa60a070e67a55352b0 +hmac: 9789d5fd470fc4273adcfd05946833268d1e466462c5f36abeb8f607d62fdb4b ... diff --git a/default.nix b/default.nix index 296c7592..5634f0af 100644 --- a/default.nix +++ b/default.nix @@ -1,149 +1,32 @@ { system ? builtins.currentSystem, - release ? false, - target ? "x86_64-unknown-linux-musl", - compileMode ? null, git_version ? null, }: with import ./nix/common.nix; -let - crossSystem = { config = target; }; -in let - log = v: builtins.trace v v; +let + compile = import ./nix/compile.nix; + build_debug_and_release = (target: { + debug = (compile { inherit target; release = false; }).workspace.garage { compileMode = "build"; }; + release = (compile { inherit target; release = true; }).workspace.garage { compileMode = "build"; }; + }); - pkgs = import pkgsSrc { - inherit system crossSystem; - overlays = [ cargo2nixOverlay ]; +in { + pkgs = { + amd64 = build_debug_and_release "x86_64-unknown-linux-musl"; + i386 = build_debug_and_release "i686-unknown-linux-musl"; + arm64 = build_debug_and_release "aarch64-unknown-linux-musl"; + arm = build_debug_and_release "armv6l-unknown-linux-musleabihf"; }; - - - /* - Rust and Nix triples are not the same. Cargo2nix has a dedicated library - to convert Nix triples to Rust ones. We need this conversion as we want to - set later options linked to our (rust) target in a generic way. Not only - the triple terminology is different, but also the "roles" are named differently. - Nix uses a build/host/target terminology where Nix's "host" maps to Cargo's "target". - */ - rustTarget = log (pkgs.rustBuilder.rustLib.rustTriple pkgs.stdenv.hostPlatform); - - /* - Cargo2nix is built for rustOverlay which installs Rust from Mozilla releases. - We want our own Rust to avoid incompatibilities, like we had with musl 1.2.0. - rustc was built with musl < 1.2.0 and nix shipped musl >= 1.2.0 which lead to compilation breakage. - So we want a Rust release that is bound to our Nix repository to avoid these problems. - See here for more info: https://musl.libc.org/time64.html - Because Cargo2nix does not support the Rust environment shipped by NixOS, - we emulate the structure of the Rust object created by rustOverlay. - In practise, rustOverlay ships rustc+cargo in a single derivation while - NixOS ships them in separate ones. We reunite them with symlinkJoin. - */ - rustChannel = pkgs.symlinkJoin { - name ="rust-channel"; - paths = [ - pkgs.rustPlatform.rust.rustc - pkgs.rustPlatform.rust.cargo - ]; - }; - - /* - Cargo2nix provides many overrides by default, you can take inspiration from them: - https://github.com/cargo2nix/cargo2nix/blob/master/overlay/overrides.nix - - You can have a complete list of the available options by looking at the overriden object, mkcrate: - https://github.com/cargo2nix/cargo2nix/blob/master/overlay/mkcrate.nix - */ - overrides = pkgs.rustBuilder.overrides.all ++ [ - /* - [1] We need to alter Nix hardening to make static binaries: PIE, - Position Independent Executables seems to be supported only on amd64. Having - this flag set either 1. make our executables crash or 2. compile as dynamic on some platforms. - Here, we deactivate it. Later (find `codegenOpts`), we reactivate it for supported targets - (only amd64 curently) through the `-static-pie` flag. - PIE is a feature used by ASLR, which helps mitigate security issues. - Learn more about Nix Hardening at: https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/cc-wrapper/add-hardening.sh - */ - (pkgs.rustBuilder.rustLib.makeOverride { - name = "garage"; - overrideAttrs = drv: { hardeningDisable = [ "pie" ]; }; - }) - - (pkgs.rustBuilder.rustLib.makeOverride { - name = "garage_rpc"; - - /* - [2] We want to inject the git version while keeping the build deterministic. - As we do not want to consider the .git folder as part of the input source, - we ask the user (the CI often) to pass the value to Nix. - */ - overrideAttrs = drv: - (if git_version != null then { - preConfigure = '' - ${drv.preConfigure or ""} - export GIT_VERSION="${git_version}" - ''; - } else {}); - - /* - [3] We ship some parts of the code disabled by default by putting them behind a flag. - It speeds up the compilation (when the feature is not required) and released crates have less dependency by default (less attack surface, disk space, etc.). - But we want to ship these additional features when we release Garage. - In the end, we chose to exclude all features from debug builds while putting (all of) them in the release builds. - Currently, the only feature of Garage is kubernetes-discovery from the garage_rpc crate. - */ - overrideArgs = old: { - features = if release then [ "kubernetes-discovery" ] else []; + test = { + amd64 = let + pkgs = import pkgsSrc { }; + rustPkgs = compile { target = "x86_64-unknown-linux-musl"; }; + in + pkgs.symlinkJoin { + name ="garage-tests"; + paths = builtins.map (key: rustPkgs.workspace.${key} { compileMode = "test"; }) (builtins.attrNames rustPkgs.workspace); }; - }) - - ]; - - packageFun = import ./Cargo.nix; - - /* - We compile fully static binaries with musl to simplify deployment on most systems. - When possible, we reactivate PIE hardening (see above). - - Also, if you set the RUSTFLAGS environment variable, the following parameters will - be ignored. - - For more information on static builds, please refer to Rust's RFC 1721. - https://rust-lang.github.io/rfcs/1721-crt-static.html#specifying-dynamicstatic-c-runtime-linkage - */ - - codegenOpts = { - "armv6l-unknown-linux-musleabihf" = [ "target-feature=+crt-static" "link-arg=-static" ]; /* compile as dynamic with static-pie */ - "aarch64-unknown-linux-musl" = [ "target-feature=+crt-static" "link-arg=-static" ]; /* segfault with static-pie */ - "i686-unknown-linux-musl" = [ "target-feature=+crt-static" "link-arg=-static" ]; /* segfault with static-pie */ - "x86_64-unknown-linux-musl" = [ "target-feature=+crt-static" "link-arg=-static-pie" ]; }; - - /* - The following definition is not elegant as we use a low level function of Cargo2nix - that enables us to pass our custom rustChannel object. We need this low level definition - to pass Nix's Rust toolchains instead of Mozilla's one. - - target is mandatory but must be kept to null to allow cargo2nix to set it to the appropriate value - for each crate. - */ - rustPkgs = pkgs.rustBuilder.makePackageSet { - inherit packageFun rustChannel release codegenOpts; - packageOverrides = overrides; - target = null; - - buildRustPackages = pkgs.buildPackages.rustBuilder.makePackageSet { - inherit rustChannel packageFun codegenOpts; - packageOverrides = overrides; - target = null; - }; - }; - - -in - if compileMode == "test" - then pkgs.symlinkJoin { - name ="garage-tests"; - paths = builtins.map (key: rustPkgs.workspace.${key} { inherit compileMode; }) (builtins.attrNames rustPkgs.workspace); - } - else rustPkgs.workspace.garage { inherit compileMode; } +} diff --git a/nix/compile.nix b/nix/compile.nix new file mode 100644 index 00000000..972e2f2e --- /dev/null +++ b/nix/compile.nix @@ -0,0 +1,140 @@ +{ + system ? builtins.currentSystem, + target ? "x86_64-unknown-linux-musl", + release ? false, + git_version ? null, +}: + +with import ./common.nix; + +let + crossSystem = { config = target; }; + + log = v: builtins.trace v v; + + pkgs = import pkgsSrc { + inherit system crossSystem; + overlays = [ cargo2nixOverlay ]; + }; + + + /* + Rust and Nix triples are not the same. Cargo2nix has a dedicated library + to convert Nix triples to Rust ones. We need this conversion as we want to + set later options linked to our (rust) target in a generic way. Not only + the triple terminology is different, but also the "roles" are named differently. + Nix uses a build/host/target terminology where Nix's "host" maps to Cargo's "target". + */ + rustTarget = log (pkgs.rustBuilder.rustLib.rustTriple pkgs.stdenv.hostPlatform); + + /* + Cargo2nix is built for rustOverlay which installs Rust from Mozilla releases. + We want our own Rust to avoid incompatibilities, like we had with musl 1.2.0. + rustc was built with musl < 1.2.0 and nix shipped musl >= 1.2.0 which lead to compilation breakage. + So we want a Rust release that is bound to our Nix repository to avoid these problems. + See here for more info: https://musl.libc.org/time64.html + Because Cargo2nix does not support the Rust environment shipped by NixOS, + we emulate the structure of the Rust object created by rustOverlay. + In practise, rustOverlay ships rustc+cargo in a single derivation while + NixOS ships them in separate ones. We reunite them with symlinkJoin. + */ + rustChannel = pkgs.symlinkJoin { + name ="rust-channel"; + paths = [ + pkgs.rustPlatform.rust.rustc + pkgs.rustPlatform.rust.cargo + ]; + }; + + /* + Cargo2nix provides many overrides by default, you can take inspiration from them: + https://github.com/cargo2nix/cargo2nix/blob/master/overlay/overrides.nix + + You can have a complete list of the available options by looking at the overriden object, mkcrate: + https://github.com/cargo2nix/cargo2nix/blob/master/overlay/mkcrate.nix + */ + overrides = pkgs.rustBuilder.overrides.all ++ [ + /* + [1] We need to alter Nix hardening to make static binaries: PIE, + Position Independent Executables seems to be supported only on amd64. Having + this flag set either 1. make our executables crash or 2. compile as dynamic on some platforms. + Here, we deactivate it. Later (find `codegenOpts`), we reactivate it for supported targets + (only amd64 curently) through the `-static-pie` flag. + PIE is a feature used by ASLR, which helps mitigate security issues. + Learn more about Nix Hardening at: https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/cc-wrapper/add-hardening.sh + */ + (pkgs.rustBuilder.rustLib.makeOverride { + name = "garage"; + overrideAttrs = drv: { hardeningDisable = [ "pie" ]; }; + }) + + (pkgs.rustBuilder.rustLib.makeOverride { + name = "garage_rpc"; + + /* + [2] We want to inject the git version while keeping the build deterministic. + As we do not want to consider the .git folder as part of the input source, + we ask the user (the CI often) to pass the value to Nix. + */ + overrideAttrs = drv: + (if git_version != null then { + preConfigure = '' + ${drv.preConfigure or ""} + export GIT_VERSION="${git_version}" + ''; + } else {}); + + /* + [3] We ship some parts of the code disabled by default by putting them behind a flag. + It speeds up the compilation (when the feature is not required) and released crates have less dependency by default (less attack surface, disk space, etc.). + But we want to ship these additional features when we release Garage. + In the end, we chose to exclude all features from debug builds while putting (all of) them in the release builds. + Currently, the only feature of Garage is kubernetes-discovery from the garage_rpc crate. + */ + overrideArgs = old: { + features = if release then [ "kubernetes-discovery" ] else []; + }; + }) + + ]; + + packageFun = import ../Cargo.nix; + + /* + We compile fully static binaries with musl to simplify deployment on most systems. + When possible, we reactivate PIE hardening (see above). + + Also, if you set the RUSTFLAGS environment variable, the following parameters will + be ignored. + + For more information on static builds, please refer to Rust's RFC 1721. + https://rust-lang.github.io/rfcs/1721-crt-static.html#specifying-dynamicstatic-c-runtime-linkage + */ + + codegenOpts = { + "armv6l-unknown-linux-musleabihf" = [ "target-feature=+crt-static" "link-arg=-static" ]; /* compile as dynamic with static-pie */ + "aarch64-unknown-linux-musl" = [ "target-feature=+crt-static" "link-arg=-static" ]; /* segfault with static-pie */ + "i686-unknown-linux-musl" = [ "target-feature=+crt-static" "link-arg=-static" ]; /* segfault with static-pie */ + "x86_64-unknown-linux-musl" = [ "target-feature=+crt-static" "link-arg=-static-pie" ]; + }; + +in + /* + The following definition is not elegant as we use a low level function of Cargo2nix + that enables us to pass our custom rustChannel object. We need this low level definition + to pass Nix's Rust toolchains instead of Mozilla's one. + + target is mandatory but must be kept to null to allow cargo2nix to set it to the appropriate value + for each crate. + */ + pkgs.rustBuilder.makePackageSet { + inherit packageFun rustChannel release codegenOpts; + packageOverrides = overrides; + target = null; + + buildRustPackages = pkgs.buildPackages.rustBuilder.makePackageSet { + inherit rustChannel packageFun codegenOpts; + packageOverrides = overrides; + target = null; + }; + } diff --git a/nix/nix.conf b/nix/nix.conf index f3defe69..de2ede71 100644 --- a/nix/nix.conf +++ b/nix/nix.conf @@ -5,3 +5,5 @@ cores = 4 log-lines = 200 filter-syscalls = false sandbox = false +keep-outputs = true +keep-derivations = true From fcb04843f7619d767c3c865e4795fb3468653e28 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Mon, 25 Jul 2022 18:10:34 +0200 Subject: [PATCH 038/149] Run clippy in nix, leveraging nix caching ability --- .drone.yml | 32 ++++------- default.nix | 17 +++--- nix/common.nix | 8 +-- nix/compile.nix | 149 ++++++++++++++++++++++++++++++++++++++---------- nix/nix.conf | 2 +- shell.nix | 14 ++--- 6 files changed, 148 insertions(+), 74 deletions(-) diff --git a/.drone.yml b/.drone.yml index f76e162d..6d76b51d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -16,7 +16,7 @@ environment: HOME: /drone/garage steps: - - name: nix maintainance + - name: nix maintenance image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store @@ -28,17 +28,7 @@ steps: - "[ -d /mnt/var/ ] || cp -r /nix/var /mnt/" - cp nix/nix.conf /etc/nix/nix.conf - - name: warmup cache - image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /nix - - name: nix_config - path: /etc/nix - commands: - - nix-build --no-build-output --no-out-link shell.nix -A rust.inputDerivation -A integration.inputDerivation - - - name: code quality + - name: check formatting image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store @@ -47,7 +37,6 @@ steps: path: /etc/nix commands: - nix-shell --attr rust --run "cargo fmt -- --check" - - nix-shell --attr rust --run "cargo clippy -- --deny warnings" - name: build image: nixpkgs/nix:nixos-22.05 @@ -57,8 +46,7 @@ steps: - name: nix_config path: /etc/nix commands: - - nix-build --no-build-output --attr pkgs.amd64.debug --argstr git_version $DRONE_COMMIT - - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" + - nix-build --no-build-output --attr clippy.amd64 --argstr git_version $DRONE_COMMIT - name: unit + func tests image: nixpkgs/nix:nixos-22.05 @@ -80,7 +68,7 @@ steps: - ./result/bin/garage-* - ./result/bin/integration-* - - name: smoke-test + - name: integration tests image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store @@ -88,7 +76,7 @@ steps: - name: nix_config path: /etc/nix commands: - - nix-build --no-build-output --attr pkgs.amd64.debug --argstr git_version $DRONE_COMMIT + - nix-build --no-build-output --attr clippy.amd64 --argstr git_version $DRONE_COMMIT - nix-shell --attr integration --run ./script/test-smoke.sh || (cat /tmp/garage.log; false) trigger: @@ -112,7 +100,7 @@ volumes: temp: {} steps: - - name: nix maintainance + - name: nix maintenance image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store @@ -198,7 +186,7 @@ volumes: temp: {} steps: - - name: nix maintainance + - name: nix maintenance image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store @@ -283,7 +271,7 @@ volumes: temp: {} steps: - - name: nix maintainance + - name: nix maintenance image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store @@ -358,7 +346,7 @@ volumes: temp: {} steps: - - name: nix maintainance + - name: nix maintenance image: nixpkgs/nix:nixos-22.05 volumes: - name: nix_store @@ -458,6 +446,6 @@ trigger: --- kind: signature -hmac: 9789d5fd470fc4273adcfd05946833268d1e466462c5f36abeb8f607d62fdb4b +hmac: 0a72ff9a422018b7b06754bd5b9561d3f4bb0d5af28a20ec365c719ee263378a ... diff --git a/default.nix b/default.nix index 5634f0af..4d7558c5 100644 --- a/default.nix +++ b/default.nix @@ -6,11 +6,16 @@ with import ./nix/common.nix; let + pkgs = import pkgsSrc { }; compile = import ./nix/compile.nix; build_debug_and_release = (target: { debug = (compile { inherit target; release = false; }).workspace.garage { compileMode = "build"; }; release = (compile { inherit target; release = true; }).workspace.garage { compileMode = "build"; }; }); + test = (rustPkgs: pkgs.symlinkJoin { + name ="garage-tests"; + paths = builtins.map (key: rustPkgs.workspace.${key} { compileMode = "test"; }) (builtins.attrNames rustPkgs.workspace); + }); in { pkgs = { @@ -20,13 +25,9 @@ in { arm = build_debug_and_release "armv6l-unknown-linux-musleabihf"; }; test = { - amd64 = let - pkgs = import pkgsSrc { }; - rustPkgs = compile { target = "x86_64-unknown-linux-musl"; }; - in - pkgs.symlinkJoin { - name ="garage-tests"; - paths = builtins.map (key: rustPkgs.workspace.${key} { compileMode = "test"; }) (builtins.attrNames rustPkgs.workspace); - }; + amd64 = test (compile { target = "x86_64-unknown-linux-musl"; }); + }; + clippy = { + amd64 = (compile { compiler = "clippy"; }).workspace.garage { compileMode = "build"; } ; }; } diff --git a/nix/common.nix b/nix/common.nix index 5cd15c8a..8396a6de 100644 --- a/nix/common.nix +++ b/nix/common.nix @@ -4,18 +4,16 @@ rec { */ pkgsSrc = fetchTarball { # As of 2021-10-04 - url ="https://github.com/NixOS/nixpkgs/archive/b27d18a412b071f5d7991d1648cfe78ee7afe68a.tar.gz"; + url = "https://github.com/NixOS/nixpkgs/archive/b27d18a412b071f5d7991d1648cfe78ee7afe68a.tar.gz"; sha256 = "1xy9zpypqfxs5gcq5dcla4bfkhxmh5nzn9dyqkr03lqycm9wg5cr"; }; cargo2nixSrc = fetchGit { # As of 2022-03-17 url = "https://github.com/superboum/cargo2nix"; - ref = "main"; - rev = "bcbf3ba99e9e01a61eb83a24624419c2dd9dec64"; + ref = "dedup_propagate"; + rev = "486675c67249e735dd7eb68e1b9feac9db102be7"; }; - - /* * Shared objects */ diff --git a/nix/compile.nix b/nix/compile.nix index 972e2f2e..49f7c1d6 100644 --- a/nix/compile.nix +++ b/nix/compile.nix @@ -1,6 +1,7 @@ { system ? builtins.currentSystem, - target ? "x86_64-unknown-linux-musl", + target ? null, + compiler ? "rustc", release ? false, git_version ? null, }: @@ -8,16 +9,14 @@ with import ./common.nix; let - crossSystem = { config = target; }; - log = v: builtins.trace v v; pkgs = import pkgsSrc { - inherit system crossSystem; + inherit system; + ${ if target == null then null else "crossSystem" } = { config = target; }; overlays = [ cargo2nixOverlay ]; }; - /* Rust and Nix triples are not the same. Cargo2nix has a dedicated library to convert Nix triples to Rust ones. We need this conversion as we want to @@ -38,13 +37,58 @@ let In practise, rustOverlay ships rustc+cargo in a single derivation while NixOS ships them in separate ones. We reunite them with symlinkJoin. */ - rustChannel = pkgs.symlinkJoin { - name ="rust-channel"; - paths = [ - pkgs.rustPlatform.rust.rustc - pkgs.rustPlatform.rust.cargo - ]; - }; + rustChannel = { + rustc = pkgs.symlinkJoin { + name = "rust-channel"; + paths = [ + pkgs.rustPlatform.rust.cargo + pkgs.rustPlatform.rust.rustc + ]; + }; + clippy = pkgs.symlinkJoin { + name = "clippy-channel"; + paths = [ + pkgs.rustPlatform.rust.cargo + pkgs.rustPlatform.rust.rustc + pkgs.clippy + ]; + }; + }.${compiler}; + + clippyBuilder = pkgs.writeScriptBin "clippy" '' + #!${pkgs.stdenv.shell} + . ${cargo2nixSrc + "/overlay/utils.sh"} + isBuildScript= + args=("$@") + for i in "''${!args[@]}"; do + if [ "xmetadata=" = "x''${args[$i]::9}" ]; then + args[$i]=metadata=$NIX_RUST_METADATA + elif [ "x--crate-name" = "x''${args[$i]}" ] && [ "xbuild_script_" = "x''${args[$i+1]::13}" ]; then + isBuildScript=1 + fi + done + if [ "$isBuildScript" ]; then + args+=($NIX_RUST_BUILD_LINK_FLAGS) + else + args+=($NIX_RUST_LINK_FLAGS) + fi + touch invoke.log + echo "''${args[@]}" >>invoke.log + + exec ${rustChannel}/bin/clippy-driver --deny warnings "''${args[@]}" + ''; + + buildEnv = (drv: { + rustc = drv.setBuildEnv; + clippy = '' + ${drv.setBuildEnv or "" } + echo + echo --- BUILDING WITH CLIPPY --- + echo + + export RUSTC=${clippyBuilder}/bin/clippy + ''; + }.${compiler}); /* Cargo2nix provides many overrides by default, you can take inspiration from them: @@ -55,47 +99,90 @@ let */ overrides = pkgs.rustBuilder.overrides.all ++ [ /* - [1] We need to alter Nix hardening to make static binaries: PIE, + [1] We add some logic to compile our crates with clippy, it provides us many additional lints + + [2] We need to alter Nix hardening to make static binaries: PIE, Position Independent Executables seems to be supported only on amd64. Having this flag set either 1. make our executables crash or 2. compile as dynamic on some platforms. Here, we deactivate it. Later (find `codegenOpts`), we reactivate it for supported targets (only amd64 curently) through the `-static-pie` flag. PIE is a feature used by ASLR, which helps mitigate security issues. Learn more about Nix Hardening at: https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/cc-wrapper/add-hardening.sh + + [3] We want to inject the git version while keeping the build deterministic. + As we do not want to consider the .git folder as part of the input source, + we ask the user (the CI often) to pass the value to Nix. + + [4] We ship some parts of the code disabled by default by putting them behind a flag. + It speeds up the compilation (when the feature is not required) and released crates have less dependency by default (less attack surface, disk space, etc.). + But we want to ship these additional features when we release Garage. + In the end, we chose to exclude all features from debug builds while putting (all of) them in the release builds. + Currently, the only feature of Garage is kubernetes-discovery from the garage_rpc crate. */ (pkgs.rustBuilder.rustLib.makeOverride { name = "garage"; - overrideAttrs = drv: { hardeningDisable = [ "pie" ]; }; + overrideAttrs = drv: { + /* [1] */ setBuildEnv = (buildEnv drv); + /* [2] */ hardeningDisable = [ "pie" ]; + }; }) (pkgs.rustBuilder.rustLib.makeOverride { name = "garage_rpc"; - - /* - [2] We want to inject the git version while keeping the build deterministic. - As we do not want to consider the .git folder as part of the input source, - we ask the user (the CI often) to pass the value to Nix. - */ overrideAttrs = drv: (if git_version != null then { - preConfigure = '' + /* [3] */ preConfigure = '' ${drv.preConfigure or ""} export GIT_VERSION="${git_version}" ''; - } else {}); - - /* - [3] We ship some parts of the code disabled by default by putting them behind a flag. - It speeds up the compilation (when the feature is not required) and released crates have less dependency by default (less attack surface, disk space, etc.). - But we want to ship these additional features when we release Garage. - In the end, we chose to exclude all features from debug builds while putting (all of) them in the release builds. - Currently, the only feature of Garage is kubernetes-discovery from the garage_rpc crate. - */ + } else {}) + // { + /* [1] */ setBuildEnv = (buildEnv drv); + }; overrideArgs = old: { - features = if release then [ "kubernetes-discovery" ] else []; + /* [4] */ features = if release then [ "kubernetes-discovery" ] else []; }; }) + (pkgs.rustBuilder.rustLib.makeOverride { + name = "garage_db"; + overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; + }) + + (pkgs.rustBuilder.rustLib.makeOverride { + name = "garage_util"; + overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; + }) + + (pkgs.rustBuilder.rustLib.makeOverride { + name = "garage_table"; + overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; + }) + + (pkgs.rustBuilder.rustLib.makeOverride { + name = "garage_block"; + overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; + }) + + (pkgs.rustBuilder.rustLib.makeOverride { + name = "garage_model"; + overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; + }) + + (pkgs.rustBuilder.rustLib.makeOverride { + name = "garage_api"; + overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; + }) + + (pkgs.rustBuilder.rustLib.makeOverride { + name = "garage_web"; + overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; + }) + + (pkgs.rustBuilder.rustLib.makeOverride { + name = "k2v-client"; + overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; + }) ]; packageFun = import ../Cargo.nix; diff --git a/nix/nix.conf b/nix/nix.conf index de2ede71..6abf96b3 100644 --- a/nix/nix.conf +++ b/nix/nix.conf @@ -1,7 +1,7 @@ substituters = https://cache.nixos.org https://nix.web.deuxfleurs.fr trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= nix.web.deuxfleurs.fr:eTGL6kvaQn6cDR/F9lDYUIP9nCVR/kkshYfLDJf1yKs= max-jobs = auto -cores = 4 +cores = 0 log-lines = 200 filter-syscalls = false sandbox = false diff --git a/shell.nix b/shell.nix index eaedb6b8..ea5d6356 100644 --- a/shell.nix +++ b/shell.nix @@ -29,16 +29,16 @@ function refresh_toolchain { ''; nativeBuildInputs = [ - pkgs.rustPlatform.rust.rustc + #pkgs.rustPlatform.rust.rustc pkgs.rustPlatform.rust.cargo - pkgs.clippy + #pkgs.clippy pkgs.rustfmt - pkgs.perl - pkgs.protobuf - pkgs.pkg-config - pkgs.openssl + #pkgs.perl + #pkgs.protobuf + #pkgs.pkg-config + #pkgs.openssl pkgs.file - cargo2nix.packages.x86_64-linux.cargo2nix + #cargo2nix.packages.x86_64-linux.cargo2nix ]; }; From a184f0d0b5f1d84524f7ba7302a4919567acec56 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Thu, 28 Jul 2022 11:41:56 +0200 Subject: [PATCH 039/149] Migrate to nix-daemon builders --- .drone.yml | 207 ++++------------------------------------------------- 1 file changed, 13 insertions(+), 194 deletions(-) diff --git a/.drone.yml b/.drone.yml index 6d76b51d..0c8a9221 100644 --- a/.drone.yml +++ b/.drone.yml @@ -2,49 +2,17 @@ kind: pipeline name: default -workspace: - base: /drone/garage - -volumes: -- name: nix_store - host: - path: /var/lib/drone/nix -- name: nix_config - temp: {} - -environment: - HOME: /drone/garage +node: + nix-daemon: 1 steps: - - name: nix maintenance - image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /mnt - - name: nix_config - path: /etc/nix - commands: - - "[ -d /mnt/store/3vpyn2qz5ay057nq9x68sh0r328d77ng-nix-2.8.1/ ] || (mkdir -p /mnt/store && cp -r /nix/store/* /mnt/store/)" - - "[ -d /mnt/var/ ] || cp -r /nix/var /mnt/" - - cp nix/nix.conf /etc/nix/nix.conf - - name: check formatting image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /nix - - name: nix_config - path: /etc/nix commands: - nix-shell --attr rust --run "cargo fmt -- --check" - name: build image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /nix - - name: nix_config - path: /etc/nix commands: - nix-build --no-build-output --attr clippy.amd64 --argstr git_version $DRONE_COMMIT @@ -52,11 +20,6 @@ steps: image: nixpkgs/nix:nixos-22.05 environment: GARAGE_TEST_INTEGRATION_EXE: result/bin/garage - volumes: - - name: nix_store - path: /nix - - name: nix_config - path: /etc/nix commands: - nix-build --no-build-output --attr test.amd64 - ./result/bin/garage_api-* @@ -70,11 +33,6 @@ steps: - name: integration tests image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /nix - - name: nix_config - path: /etc/nix commands: - nix-build --no-build-output --attr clippy.amd64 --argstr git_version $DRONE_COMMIT - nix-shell --attr integration --run ./script/test-smoke.sh || (cat /tmp/garage.log; false) @@ -92,54 +50,23 @@ kind: pipeline type: docker name: release-linux-amd64 -volumes: -- name: nix_store - host: - path: /var/lib/drone/nix -- name: nix_config - temp: {} +node: + nix-daemon: 1 steps: - - name: nix maintenance - image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /mnt - - name: nix_config - path: /etc/nix - commands: - - "[ -d /mnt/store/3vpyn2qz5ay057nq9x68sh0r328d77ng-nix-2.8.1/ ] || (mkdir -p /mnt/store && cp -r /nix/store/* /mnt/store/)" - - "[ -d /mnt/var/ ] || cp -r /nix/var /mnt/" - - cp nix/nix.conf /etc/nix/nix.conf - - name: build image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /nix - - name: nix_config - path: /etc/nix commands: - nix-build --no-build-output --attr pkgs.amd64.release --argstr git_version $DRONE_COMMIT - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: integration image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /nix - - name: nix_config - path: /etc/nix commands: - nix-shell --attr integration --run ./script/test-smoke.sh || (cat /tmp/garage.log; false) - name: push static binary image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /nix - - name: nix_config - path: /etc/nix environment: AWS_ACCESS_KEY_ID: from_secret: garagehq_aws_access_key_id @@ -150,11 +77,6 @@ steps: - name: docker build and publish image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /nix - - name: nix_config - path: /etc/nix environment: DOCKER_AUTH: from_secret: docker_auth @@ -178,54 +100,23 @@ kind: pipeline type: docker name: release-linux-i386 -volumes: -- name: nix_store - host: - path: /var/lib/drone/nix -- name: nix_config - temp: {} +node: + nix-daemon: 1 steps: - - name: nix maintenance - image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /mnt - - name: nix_config - path: /etc/nix - commands: - - "[ -d /mnt/store/3vpyn2qz5ay057nq9x68sh0r328d77ng-nix-2.8.1/ ] || (mkdir -p /mnt/store && cp -r /nix/store/* /mnt/store/)" - - "[ -d /mnt/var/ ] || cp -r /nix/var /mnt/" - - cp nix/nix.conf /etc/nix/nix.conf - - name: build image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /nix - - name: nix_config - path: /etc/nix commands: - nix-build --no-build-output --attr pkgs.i386.release --argstr git_version $DRONE_COMMIT - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: integration image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /nix - - name: nix_config - path: /etc/nix commands: - nix-shell --attr integration --run ./script/test-smoke.sh || (cat /tmp/garage.log; false) - name: push static binary image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /nix - - name: nix_config - path: /etc/nix environment: AWS_ACCESS_KEY_ID: from_secret: garagehq_aws_access_key_id @@ -236,11 +127,6 @@ steps: - name: docker build and publish image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /nix - - name: nix_config - path: /etc/nix environment: DOCKER_AUTH: from_secret: docker_auth @@ -263,44 +149,18 @@ kind: pipeline type: docker name: release-linux-arm64 -volumes: -- name: nix_store - host: - path: /var/lib/drone/nix -- name: nix_config - temp: {} +node: + nix-daemon: 1 steps: - - name: nix maintenance - image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /mnt - - name: nix_config - path: /etc/nix - commands: - - "[ -d /mnt/store/3vpyn2qz5ay057nq9x68sh0r328d77ng-nix-2.8.1/ ] || (mkdir -p /mnt/store && cp -r /nix/store/* /mnt/store/)" - - "[ -d /mnt/var/ ] || cp -r /nix/var /mnt/" - - cp nix/nix.conf /etc/nix/nix.conf - - name: build image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /nix - - name: nix_config - path: /etc/nix commands: - nix-build --no-build-output --attr pkgs.arm64.release --argstr git_version $DRONE_COMMIT - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: push static binary image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /nix - - name: nix_config - path: /etc/nix environment: AWS_ACCESS_KEY_ID: from_secret: garagehq_aws_access_key_id @@ -311,11 +171,6 @@ steps: - name: docker build and publish image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /nix - - name: nix_config - path: /etc/nix environment: DOCKER_AUTH: from_secret: docker_auth @@ -338,44 +193,18 @@ kind: pipeline type: docker name: release-linux-arm -volumes: -- name: nix_store - host: - path: /var/lib/drone/nix -- name: nix_config - temp: {} +node: + nix-daemon: 1 steps: - - name: nix maintenance - image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /mnt - - name: nix_config - path: /etc/nix - commands: - - "[ -d /mnt/store/3vpyn2qz5ay057nq9x68sh0r328d77ng-nix-2.8.1/ ] || (mkdir -p /mnt/store && cp -r /nix/store/* /mnt/store/)" - - "[ -d /mnt/var/ ] || cp -r /nix/var /mnt/" - - cp nix/nix.conf /etc/nix/nix.conf - - name: build image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /nix - - name: nix_config - path: /etc/nix commands: - nix-build --no-build-output --attr pkgs.arm.release --argstr git_version $DRONE_COMMIT - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: push static binary image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /nix - - name: nix_config - path: /etc/nix environment: AWS_ACCESS_KEY_ID: from_secret: garagehq_aws_access_key_id @@ -386,11 +215,6 @@ steps: - name: docker build and publish image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /nix - - name: nix_config - path: /etc/nix environment: DOCKER_AUTH: from_secret: docker_auth @@ -413,17 +237,12 @@ kind: pipeline type: docker name: refresh-release-page -volumes: -- name: nix_store - host: - path: /var/lib/drone/nix +node: + nix-daemon: 1 steps: - name: refresh-index image: nixpkgs/nix:nixos-22.05 - volumes: - - name: nix_store - path: /nix environment: AWS_ACCESS_KEY_ID: from_secret: garagehq_aws_access_key_id @@ -446,6 +265,6 @@ trigger: --- kind: signature -hmac: 0a72ff9a422018b7b06754bd5b9561d3f4bb0d5af28a20ec365c719ee263378a +hmac: 8495114848396ebb492831fc9bd37b353e1a4add9d72c0a123d109490a5b0db0 ... From 1b2e1296eb99630e969e585ede0424072adc2d0c Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 18 Jul 2022 17:18:47 +0200 Subject: [PATCH 040/149] Compute hashes on dedicated threads --- Cargo.lock | 95 +++++++++++++++++++++++++++++++++------- src/api/Cargo.toml | 8 ++-- src/api/s3/copy.rs | 5 ++- src/api/s3/put.rs | 32 +++++++++----- src/api/signature/mod.rs | 14 +++--- src/block/block.rs | 17 ++++--- src/block/manager.rs | 6 ++- src/garage/Cargo.toml | 2 +- src/util/Cargo.toml | 4 +- src/util/async_hash.rs | 55 +++++++++++++++++++++++ src/util/lib.rs | 1 + 11 files changed, 188 insertions(+), 51 deletions(-) create mode 100644 src/util/async_hash.rs diff --git a/Cargo.lock b/Cargo.lock index e1ccfc2d..260ae907 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -343,7 +343,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a4e37d16930f5459780f5621038b6382b9bb37c19016f39fb6b5808d831f174" dependencies = [ "crypto-mac 0.8.0", - "digest", + "digest 0.9.0", "opaque-debug", ] @@ -356,6 +356,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.9.1" @@ -589,6 +598,16 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "crypto-mac" version = "0.8.0" @@ -693,6 +712,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer 0.10.2", + "crypto-common", + "subtle", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -991,7 +1021,7 @@ dependencies = [ "serde", "serde_bytes", "serde_json", - "sha2", + "sha2 0.10.2", "static_init", "structopt", "timeago", @@ -1008,7 +1038,7 @@ dependencies = [ "base64", "bytes 1.1.0", "chrono", - "crypto-mac 0.10.1", + "crypto-common", "err-derive 0.3.1", "form_urlencoded", "futures", @@ -1019,13 +1049,13 @@ dependencies = [ "garage_table 0.7.0", "garage_util 0.7.0", "hex", - "hmac 0.10.1", + "hmac 0.12.1", "http", "http-range", "httpdate 0.3.2", "hyper", "idna", - "md-5", + "md-5 0.10.1", "multer", "nom", "opentelemetry", @@ -1039,7 +1069,7 @@ dependencies = [ "serde", "serde_bytes", "serde_json", - "sha2", + "sha2 0.10.2", "tokio", "tracing", "url", @@ -1259,7 +1289,7 @@ dependencies = [ "rmp-serde 0.15.5", "serde", "serde_json", - "sha2", + "sha2 0.9.9", "sled", "tokio", "toml", @@ -1272,7 +1302,9 @@ version = "0.7.0" dependencies = [ "async-trait", "blake2", + "bytes 1.1.0", "chrono", + "digest 0.10.3", "err-derive 0.3.1", "futures", "garage_db", @@ -1285,7 +1317,7 @@ dependencies = [ "rmp-serde 0.15.5", "serde", "serde_json", - "sha2", + "sha2 0.10.2", "tokio", "toml", "tracing", @@ -1485,7 +1517,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" dependencies = [ "crypto-mac 0.10.1", - "digest", + "digest 0.9.0", ] [[package]] @@ -1495,7 +1527,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" dependencies = [ "crypto-mac 0.11.1", - "digest", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.3", ] [[package]] @@ -1975,11 +2016,20 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" dependencies = [ - "block-buffer", - "digest", + "block-buffer 0.9.0", + "digest 0.9.0", "opaque-debug", ] +[[package]] +name = "md-5" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658646b21e0b72f7866c7038ab086d3d5e1cd6271f060fd37defb241949d0582" +dependencies = [ + "digest 0.10.3", +] + [[package]] name = "md5" version = "0.7.0" @@ -3000,20 +3050,20 @@ dependencies = [ "base64", "bytes 1.1.0", "chrono", - "digest", + "digest 0.9.0", "futures", "hex", "hmac 0.11.0", "http", "hyper", "log", - "md-5", + "md-5 0.9.1", "percent-encoding", "pin-project-lite", "rusoto_credential", "rustc_version", "serde", - "sha2", + "sha2 0.9.9", "tokio", ] @@ -3246,13 +3296,24 @@ version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if 1.0.0", "cpufeatures", - "digest", + "digest 0.9.0", "opaque-debug", ] +[[package]] +name = "sha2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.10.3", +] + [[package]] name = "shlex" version = "1.1.0" diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml index db77cf38..901cb959 100644 --- a/src/api/Cargo.toml +++ b/src/api/Cargo.toml @@ -24,15 +24,15 @@ async-trait = "0.1.7" base64 = "0.13" bytes = "1.0" chrono = "0.4" -crypto-mac = "0.10" +crypto-common = "0.1" err-derive = "0.3" hex = "0.4" -hmac = "0.10" +hmac = "0.12" idna = "0.2" tracing = "0.1.30" -md-5 = "0.9" +md-5 = "0.10" nom = "7.1" -sha2 = "0.9" +sha2 = "0.10" futures = "0.3" futures-util = "0.3" diff --git a/src/api/s3/copy.rs b/src/api/s3/copy.rs index 0fc16993..4415a037 100644 --- a/src/api/s3/copy.rs +++ b/src/api/s3/copy.rs @@ -365,7 +365,10 @@ pub async fn handle_upload_part_copy( // we need to insert that data as a new block. async move { if must_upload { - garage2.block_manager.rpc_put_block(final_hash, data).await + garage2 + .block_manager + .rpc_put_block(final_hash, data.into()) + .await } else { Ok(()) } diff --git a/src/api/s3/put.rs b/src/api/s3/put.rs index 9ef37421..fbfa6f0d 100644 --- a/src/api/s3/put.rs +++ b/src/api/s3/put.rs @@ -9,6 +9,7 @@ use md5::{digest::generic_array::*, Digest as Md5Digest, Md5}; use sha2::Sha256; use garage_table::*; +use garage_util::async_hash::*; use garage_util::data::*; use garage_util::error::Error as GarageError; use garage_util::time::*; @@ -130,7 +131,8 @@ pub(crate) async fn save_stream> + Unpin>( garage.version_table.insert(&version).await?; // Transfer data and verify checksum - let first_block_hash = blake2sum(&first_block[..]); + let first_block = Bytes::from(first_block); + let first_block_hash = async_blake2sum(first_block.clone()).await; let tx_result = (|| async { let (total_size, data_md5sum, data_sha256sum) = read_and_put_blocks( @@ -273,14 +275,16 @@ async fn read_and_put_blocks> + Unpin>( garage: &Garage, version: &Version, part_number: u64, - first_block: Vec, + first_block: Bytes, first_block_hash: Hash, chunker: &mut StreamChunker, ) -> Result<(u64, GenericArray, Hash), Error> { - let mut md5hasher = Md5::new(); - let mut sha256hasher = Sha256::new(); - md5hasher.update(&first_block[..]); - sha256hasher.update(&first_block[..]); + let first_block = Bytes::from(first_block); + + let md5hasher = AsyncHasher::::new(); + let sha256hasher = AsyncHasher::::new(); + md5hasher.update(first_block.clone()); + sha256hasher.update(first_block.clone()); let mut next_offset = first_block.len(); let mut put_curr_version_block = put_block_meta( @@ -302,9 +306,10 @@ async fn read_and_put_blocks> + Unpin>( chunker.next(), )?; if let Some(block) = next_block { - md5hasher.update(&block[..]); - sha256hasher.update(&block[..]); - let block_hash = blake2sum(&block[..]); + let block = Bytes::from(block); + md5hasher.update(block.clone()); + sha256hasher.update(block.clone()); + let block_hash = async_blake2sum(block.clone()).await; let block_len = block.len(); put_curr_version_block = put_block_meta( garage, @@ -322,9 +327,9 @@ async fn read_and_put_blocks> + Unpin>( } let total_size = next_offset as u64; - let data_md5sum = md5hasher.finalize(); + let data_md5sum = md5hasher.finalize().await; - let data_sha256sum = sha256hasher.finalize(); + let data_sha256sum = sha256hasher.finalize().await; let data_sha256sum = Hash::try_from(&data_sha256sum[..]).unwrap(); Ok((total_size, data_md5sum, data_sha256sum)) @@ -504,7 +509,10 @@ pub async fn handle_put_part( // Copy block to store let version = Version::new(version_uuid, bucket_id, key, false); - let first_block_hash = blake2sum(&first_block[..]); + + let first_block = Bytes::from(first_block); + let first_block_hash = async_blake2sum(first_block.clone()).await; + let (_, data_md5sum, data_sha256sum) = read_and_put_blocks( &garage, &version, diff --git a/src/api/signature/mod.rs b/src/api/signature/mod.rs index dd5b590c..4b8b990f 100644 --- a/src/api/signature/mod.rs +++ b/src/api/signature/mod.rs @@ -1,5 +1,5 @@ use chrono::{DateTime, Utc}; -use hmac::{Hmac, Mac, NewMac}; +use hmac::{Hmac, Mac}; use sha2::Sha256; use garage_util::data::{sha256sum, Hash}; @@ -29,17 +29,17 @@ pub fn signing_hmac( secret_key: &str, region: &str, service: &str, -) -> Result { +) -> Result { let secret = String::from("AWS4") + secret_key; - let mut date_hmac = HmacSha256::new_varkey(secret.as_bytes())?; + let mut date_hmac = HmacSha256::new_from_slice(secret.as_bytes())?; date_hmac.update(datetime.format(SHORT_DATE).to_string().as_bytes()); - let mut region_hmac = HmacSha256::new_varkey(&date_hmac.finalize().into_bytes())?; + let mut region_hmac = HmacSha256::new_from_slice(&date_hmac.finalize().into_bytes())?; region_hmac.update(region.as_bytes()); - let mut service_hmac = HmacSha256::new_varkey(®ion_hmac.finalize().into_bytes())?; + let mut service_hmac = HmacSha256::new_from_slice(®ion_hmac.finalize().into_bytes())?; service_hmac.update(service.as_bytes()); - let mut signing_hmac = HmacSha256::new_varkey(&service_hmac.finalize().into_bytes())?; + let mut signing_hmac = HmacSha256::new_from_slice(&service_hmac.finalize().into_bytes())?; signing_hmac.update(b"aws4_request"); - let hmac = HmacSha256::new_varkey(&signing_hmac.finalize().into_bytes())?; + let hmac = HmacSha256::new_from_slice(&signing_hmac.finalize().into_bytes())?; Ok(hmac) } diff --git a/src/block/block.rs b/src/block/block.rs index 4d3fbcb8..f17bd2c0 100644 --- a/src/block/block.rs +++ b/src/block/block.rs @@ -1,3 +1,4 @@ +use bytes::Bytes; use serde::{Deserialize, Serialize}; use zstd::stream::{decode_all as zstd_decode, Encoder}; @@ -61,13 +62,17 @@ impl DataBlock { } } - pub fn from_buffer(data: Vec, level: Option) -> DataBlock { - if let Some(level) = level { - if let Ok(data) = zstd_encode(&data[..], level) { - return DataBlock::Compressed(data); + pub async fn from_buffer(data: Bytes, level: Option) -> DataBlock { + tokio::task::spawn_blocking(move || { + if let Some(level) = level { + if let Ok(data) = zstd_encode(&data[..], level) { + return DataBlock::Compressed(data); + } } - } - DataBlock::Plain(data) + DataBlock::Plain(data.to_vec()) // TODO: remove to_vec here + }) + .await + .unwrap() } } diff --git a/src/block/manager.rs b/src/block/manager.rs index 017ba9da..890c247d 100644 --- a/src/block/manager.rs +++ b/src/block/manager.rs @@ -5,6 +5,7 @@ use std::time::Duration; use arc_swap::ArcSwapOption; use async_trait::async_trait; +use bytes::Bytes; use serde::{Deserialize, Serialize}; use futures::future::*; @@ -211,14 +212,15 @@ impl BlockManager { } /// Send block to nodes that should have it - pub async fn rpc_put_block(&self, hash: Hash, data: Vec) -> Result<(), Error> { + pub async fn rpc_put_block(&self, hash: Hash, data: Bytes) -> Result<(), Error> { let who = self.replication.write_nodes(&hash); - let data = DataBlock::from_buffer(data, self.compression_level); + let data = DataBlock::from_buffer(data, self.compression_level).await; self.system .rpc .try_call_many( &self.endpoint, &who[..], + // TODO: remove to_vec() here BlockRpc::PutBlock { hash, data }, RequestStrategy::with_priority(PRIO_NORMAL) .with_quorum(self.replication.write_quorum()) diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index 8948e750..80802a16 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -65,7 +65,7 @@ chrono = "0.4" http = "0.2" hmac = "0.10" hyper = { version = "0.14", features = ["client", "http1", "runtime"] } -sha2 = "0.9" +sha2 = "0.10" static_init = "1.0" assert-json-diff = "2.0" diff --git a/src/util/Cargo.toml b/src/util/Cargo.toml index 57c70ffb..7d79f21a 100644 --- a/src/util/Cargo.toml +++ b/src/util/Cargo.toml @@ -18,12 +18,14 @@ garage_db = { version = "0.8.0", path = "../db" } async-trait = "0.1" blake2 = "0.9" +bytes = "1.0" +digest = "0.10" err-derive = "0.3" xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] } hex = "0.4" tracing = "0.1.30" rand = "0.8" -sha2 = "0.9" +sha2 = "0.10" chrono = "0.4" rmp-serde = "0.15" diff --git a/src/util/async_hash.rs b/src/util/async_hash.rs new file mode 100644 index 00000000..67776eb9 --- /dev/null +++ b/src/util/async_hash.rs @@ -0,0 +1,55 @@ +use bytes::Bytes; +use digest::Digest; + +use tokio::sync::mpsc; +use tokio::task::JoinHandle; + +use crate::data::*; + +/// Compute the sha256 of a slice, +/// spawning on a tokio thread for CPU-intensive processing +/// The argument has to be an owned Bytes, as it is moved out to a new thread. +pub async fn async_sha256sum(data: Bytes) -> Hash { + tokio::task::spawn_blocking(move || sha256sum(&data)) + .await + .unwrap() +} + +/// Compute the blake2sum of a slice, +/// spawning on a tokio thread for CPU-intensive processing. +/// The argument has to be an owned Bytes, as it is moved out to a new thread. +pub async fn async_blake2sum(data: Bytes) -> Hash { + tokio::task::spawn_blocking(move || blake2sum(&data)) + .await + .unwrap() +} + +// ---- + +pub struct AsyncHasher { + sendblk: mpsc::UnboundedSender, + task: JoinHandle>, +} + +impl AsyncHasher { + pub fn new() -> Self { + let (sendblk, mut recvblk) = mpsc::unbounded_channel::(); + let task = tokio::task::spawn_blocking(move || { + let mut digest = D::new(); + while let Some(blk) = recvblk.blocking_recv() { + digest.update(&blk[..]); + } + digest.finalize() + }); + Self { sendblk, task } + } + + pub fn update(&self, b: Bytes) { + self.sendblk.send(b).unwrap() + } + + pub async fn finalize(self) -> digest::Output { + drop(self.sendblk); + self.task.await.unwrap() + } +} diff --git a/src/util/lib.rs b/src/util/lib.rs index fce151af..7152f92a 100644 --- a/src/util/lib.rs +++ b/src/util/lib.rs @@ -3,6 +3,7 @@ #[macro_use] extern crate tracing; +pub mod async_hash; pub mod background; pub mod config; pub mod crdt; From 2f111e6b3d772b10c8ed6279ce0c82d22852afd1 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 18 Jul 2022 18:40:57 +0200 Subject: [PATCH 041/149] Performance improvements: - reduce contention on mutation_lock by having 256 of them - better lmdb defaults --- src/api/s3/put.rs | 30 +++++++++++++++++++++++++----- src/block/manager.rs | 27 +++++++++++++++------------ src/model/garage.rs | 15 ++++++++++----- src/util/async_hash.rs | 15 +++++++++------ 4 files changed, 59 insertions(+), 28 deletions(-) diff --git a/src/api/s3/put.rs b/src/api/s3/put.rs index fbfa6f0d..a182f04d 100644 --- a/src/api/s3/put.rs +++ b/src/api/s3/put.rs @@ -8,6 +8,11 @@ use hyper::{Request, Response}; use md5::{digest::generic_array::*, Digest as Md5Digest, Md5}; use sha2::Sha256; +use opentelemetry::{ + trace::{FutureExt as OtelFutureExt, TraceContextExt, Tracer}, + Context, +}; + use garage_table::*; use garage_util::async_hash::*; use garage_util::data::*; @@ -279,12 +284,21 @@ async fn read_and_put_blocks> + Unpin>( first_block_hash: Hash, chunker: &mut StreamChunker, ) -> Result<(u64, GenericArray, Hash), Error> { + let tracer = opentelemetry::global::tracer("garage"); + let first_block = Bytes::from(first_block); let md5hasher = AsyncHasher::::new(); let sha256hasher = AsyncHasher::::new(); - md5hasher.update(first_block.clone()); - sha256hasher.update(first_block.clone()); + + futures::future::join( + md5hasher.update(first_block.clone()), + sha256hasher.update(first_block.clone()), + ) + .with_context(Context::current_with_span( + tracer.start("Hash first block (md5, sha256)"), + )) + .await; let mut next_offset = first_block.len(); let mut put_curr_version_block = put_block_meta( @@ -307,9 +321,15 @@ async fn read_and_put_blocks> + Unpin>( )?; if let Some(block) = next_block { let block = Bytes::from(block); - md5hasher.update(block.clone()); - sha256hasher.update(block.clone()); - let block_hash = async_blake2sum(block.clone()).await; + let (_, _, block_hash) = futures::future::join3( + md5hasher.update(block.clone()), + sha256hasher.update(block.clone()), + async_blake2sum(block.clone()), + ) + .with_context(Context::current_with_span( + tracer.start("Hash block (md5, sha256, blake2)"), + )) + .await; let block_len = block.len(); put_curr_version_block = put_block_meta( garage, diff --git a/src/block/manager.rs b/src/block/manager.rs index 890c247d..be53ec6e 100644 --- a/src/block/manager.rs +++ b/src/block/manager.rs @@ -93,7 +93,7 @@ pub struct BlockManager { compression_level: Option, background_tranquility: u32, - mutation_lock: Mutex, + mutation_lock: [Mutex; 256], pub(crate) rc: BlockRc, @@ -150,8 +150,6 @@ impl BlockManager { .netapp .endpoint("garage_block/manager.rs/Rpc".to_string()); - let manager_locked = BlockManagerLocked(); - let metrics = BlockManagerMetrics::new(resync_queue.clone(), resync_errors.clone()); let block_manager = Arc::new(Self { @@ -159,7 +157,7 @@ impl BlockManager { data_dir, compression_level, background_tranquility, - mutation_lock: Mutex::new(manager_locked), + mutation_lock: [(); 256].map(|_| Mutex::new(BlockManagerLocked())), rc, resync_queue, resync_notify: Notify::new(), @@ -313,14 +311,21 @@ impl BlockManager { /// Write a block to disk async fn write_block(&self, hash: &Hash, data: &DataBlock) -> Result { + let tracer = opentelemetry::global::tracer("garage"); + let write_size = data.inner_buffer().len() as u64; - let res = self - .mutation_lock + let res = self.mutation_lock[hash.as_slice()[0] as usize] .lock() + .with_context(Context::current_with_span( + tracer.start("Acquire mutation_lock"), + )) .await .write_block(hash, data, self) .bound_record_duration(&self.metrics.block_write_duration) + .with_context(Context::current_with_span( + tracer.start("BlockManagerLocked::write_block"), + )) .await?; self.metrics.bytes_written.add(write_size); @@ -370,7 +375,7 @@ impl BlockManager { if data.verify(*hash).is_err() { self.metrics.corruption_counter.add(1); - self.mutation_lock + self.mutation_lock[hash.as_slice()[0] as usize] .lock() .await .move_block_to_corrupted(hash, self) @@ -384,8 +389,7 @@ impl BlockManager { /// Check if this node should have a block, but don't actually have it async fn need_block(&self, hash: &Hash) -> Result { - let BlockStatus { exists, needed } = self - .mutation_lock + let BlockStatus { exists, needed } = self.mutation_lock[hash.as_slice()[0] as usize] .lock() .await .check_block_status(hash, self) @@ -608,8 +612,7 @@ impl BlockManager { } async fn resync_block(&self, hash: &Hash) -> Result<(), Error> { - let BlockStatus { exists, needed } = self - .mutation_lock + let BlockStatus { exists, needed } = self.mutation_lock[hash.as_slice()[0] as usize] .lock() .await .check_block_status(hash, self) @@ -694,7 +697,7 @@ impl BlockManager { who.len() ); - self.mutation_lock + self.mutation_lock[hash.as_slice()[0] as usize] .lock() .await .delete_if_unneeded(hash, self) diff --git a/src/model/garage.rs b/src/model/garage.rs index 15769a17..0d239df6 100644 --- a/src/model/garage.rs +++ b/src/model/garage.rs @@ -104,11 +104,16 @@ impl Garage { std::fs::create_dir_all(&db_path).expect("Unable to create LMDB data directory"); let map_size = garage_db::lmdb_adapter::recommended_map_size(); - let db = db::lmdb_adapter::heed::EnvOpenOptions::new() - .max_dbs(100) - .map_size(map_size) - .open(&db_path) - .expect("Unable to open LMDB DB"); + use db::lmdb_adapter::heed; + let mut env_builder = heed::EnvOpenOptions::new(); + env_builder.max_dbs(100); + env_builder.max_readers(500); + env_builder.map_size(map_size); + unsafe { + env_builder.flag(heed::flags::Flags::MdbNoSync); + env_builder.flag(heed::flags::Flags::MdbNoMetaSync); + } + let db = env_builder.open(&db_path).expect("Unable to open LMDB DB"); db::lmdb_adapter::LmdbDb::init(db) } e => { diff --git a/src/util/async_hash.rs b/src/util/async_hash.rs index 67776eb9..be0535de 100644 --- a/src/util/async_hash.rs +++ b/src/util/async_hash.rs @@ -1,7 +1,7 @@ use bytes::Bytes; use digest::Digest; -use tokio::sync::mpsc; +use tokio::sync::{mpsc, oneshot}; use tokio::task::JoinHandle; use crate::data::*; @@ -27,25 +27,28 @@ pub async fn async_blake2sum(data: Bytes) -> Hash { // ---- pub struct AsyncHasher { - sendblk: mpsc::UnboundedSender, + sendblk: mpsc::UnboundedSender<(Bytes, oneshot::Sender<()>)>, task: JoinHandle>, } impl AsyncHasher { pub fn new() -> Self { - let (sendblk, mut recvblk) = mpsc::unbounded_channel::(); + let (sendblk, mut recvblk) = mpsc::unbounded_channel::<(Bytes, oneshot::Sender<()>)>(); let task = tokio::task::spawn_blocking(move || { let mut digest = D::new(); - while let Some(blk) = recvblk.blocking_recv() { + while let Some((blk, ch)) = recvblk.blocking_recv() { digest.update(&blk[..]); + let _ = ch.send(()); } digest.finalize() }); Self { sendblk, task } } - pub fn update(&self, b: Bytes) { - self.sendblk.send(b).unwrap() + pub async fn update(&self, b: Bytes) { + let (tx, rx) = oneshot::channel(); + self.sendblk.send((b, tx)).unwrap(); + let _ = rx.await; } pub async fn finalize(self) -> digest::Output { From 40150527b857f10876fdfd5d0736a5df7c24d111 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 18 Jul 2022 18:49:59 +0200 Subject: [PATCH 042/149] Update cargo.nix --- Cargo.nix | 118 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 104 insertions(+), 14 deletions(-) diff --git a/Cargo.nix b/Cargo.nix index 37bf3186..cf86ff0e 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -55,7 +55,7 @@ in garage_api = rustPackages.unknown.garage_api."0.7.0"; garage_web = rustPackages.unknown.garage_web."0.7.0"; garage = rustPackages.unknown.garage."0.7.0"; - k2v-client = rustPackages.unknown.k2v-client."0.1.0"; + k2v-client = rustPackages.unknown.k2v-client."0.0.1"; }; "registry+https://github.com/rust-lang/crates.io-index".ahash."0.7.6" = overridableMkRustCrate (profileName: rec { name = "ahash"; @@ -505,6 +505,16 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".block-buffer."0.10.2" = overridableMkRustCrate (profileName: rec { + name = "block-buffer"; + version = "0.10.2"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"; }; + dependencies = { + generic_array = rustPackages."registry+https://github.com/rust-lang/crates.io-index".generic-array."0.14.5" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".bumpalo."3.9.1" = overridableMkRustCrate (profileName: rec { name = "bumpalo"; version = "3.9.1"; @@ -749,7 +759,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.config == "aarch64-linux-android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -848,6 +858,20 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".crypto-common."0.1.6" = overridableMkRustCrate (profileName: rec { + name = "crypto-common"; + version = "0.1.6"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"; }; + features = builtins.concatLists [ + [ "std" ] + ]; + dependencies = { + generic_array = rustPackages."registry+https://github.com/rust-lang/crates.io-index".generic-array."0.14.5" { inherit profileName; }; + typenum = rustPackages."registry+https://github.com/rust-lang/crates.io-index".typenum."1.15.0" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".crypto-mac."0.8.0" = overridableMkRustCrate (profileName: rec { name = "crypto-mac"; version = "0.8.0"; @@ -980,6 +1004,27 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".digest."0.10.3" = overridableMkRustCrate (profileName: rec { + name = "digest"; + version = "0.10.3"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"; }; + features = builtins.concatLists [ + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "alloc") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "block-buffer") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "core-api") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "mac") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "std") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "subtle") + ]; + dependencies = { + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "block_buffer" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".block-buffer."0.10.2" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "crypto_common" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".crypto-common."0.1.6" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "subtle" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".subtle."2.4.1" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".dirs-next."2.0.0" = overridableMkRustCrate (profileName: rec { name = "dirs-next"; version = "2.0.0"; @@ -1408,7 +1453,7 @@ in http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; hyper = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; - sha2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sha2."0.9.9" { inherit profileName; }; + sha2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sha2."0.10.2" { inherit profileName; }; static_init = rustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init."1.0.2" { inherit profileName; }; }; }); @@ -1426,7 +1471,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "base64" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.13.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "chrono" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.19" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "crypto_mac" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".crypto-mac."0.10.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "crypto_common" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".crypto-common."0.1.6" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "err_derive" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "form_urlencoded" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".form_urlencoded."1.0.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; @@ -1437,13 +1482,13 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "garage_table" else null } = rustPackages."unknown".garage_table."0.7.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "hmac" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hmac."0.10.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "hmac" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hmac."0.12.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "http" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "http_range" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-range."0.1.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "httpdate" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".httpdate."0.3.2" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "hyper" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "idna" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".idna."0.2.3" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "md5" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".md-5."0.9.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "md5" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".md-5."0.10.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "multer" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".multer."2.0.2" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "nom" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".nom."7.1.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; @@ -1457,7 +1502,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "serde_bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "serde_json" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "sha2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sha2."0.9.9" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "sha2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sha2."0.10.2" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "tracing" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "url" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".url."2.2.2" { inherit profileName; }; @@ -1732,7 +1777,9 @@ in dependencies = { ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "async_trait" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "blake2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".blake2."0.9.2" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "chrono" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.19" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "digest" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".digest."0.10.3" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "err_derive" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "garage_db" else null } = rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }; @@ -1745,7 +1792,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "serde_json" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "sha2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sha2."0.9.9" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "sha2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sha2."0.10.2" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "toml" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".toml."0.5.8" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tracing" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; @@ -1778,6 +1825,9 @@ in version = "0.14.5"; registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"; }; + features = builtins.concatLists [ + [ "more_lengths" ] + ]; dependencies = { typenum = rustPackages."registry+https://github.com/rust-lang/crates.io-index".typenum."1.15.0" { inherit profileName; }; }; @@ -2018,6 +2068,16 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".hmac."0.12.1" = overridableMkRustCrate (profileName: rec { + name = "hmac"; + version = "0.12.1"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"; }; + dependencies = { + digest = rustPackages."registry+https://github.com/rust-lang/crates.io-index".digest."0.10.3" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" = overridableMkRustCrate (profileName: rec { name = "http"; version = "0.2.6"; @@ -2327,9 +2387,9 @@ in }; }); - "unknown".k2v-client."0.1.0" = overridableMkRustCrate (profileName: rec { + "unknown".k2v-client."0.0.1" = overridableMkRustCrate (profileName: rec { name = "k2v-client"; - version = "0.1.0"; + version = "0.0.1"; registry = "unknown"; src = fetchCrateLocal (workspaceSrc + "/src/k2v-client"); features = builtins.concatLists [ @@ -2692,6 +2752,20 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".md-5."0.10.1" = overridableMkRustCrate (profileName: rec { + name = "md-5"; + version = "0.10.1"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "658646b21e0b72f7866c7038ab086d3d5e1cd6271f060fd37defb241949d0582"; }; + features = builtins.concatLists [ + [ "default" ] + [ "std" ] + ]; + dependencies = { + digest = rustPackages."registry+https://github.com/rust-lang/crates.io-index".digest."0.10.3" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".md5."0.7.0" = overridableMkRustCrate (profileName: rec { name = "md5"; version = "0.7.0"; @@ -3938,7 +4012,7 @@ in ]; dependencies = { ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" || hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "i686" || hostPlatform.parsed.cpu.name == "x86_64" || (hostPlatform.parsed.cpu.name == "aarch64" || hostPlatform.parsed.cpu.name == "armv6l" || hostPlatform.parsed.cpu.name == "armv7l") && (hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "fuchsia" || hostPlatform.parsed.kernel.name == "linux") then "spin" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".spin."0.5.2" { inherit profileName; }; untrusted = rustPackages."registry+https://github.com/rust-lang/crates.io-index".untrusted."0.7.1" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "wasm32" && hostPlatform.parsed.vendor.name == "unknown" && hostPlatform.parsed.kernel.name == "unknown" && hostPlatform.parsed.abi.name == "" then "web_sys" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.56" { inherit profileName; }; @@ -4392,6 +4466,22 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".sha2."0.10.2" = overridableMkRustCrate (profileName: rec { + name = "sha2"; + version = "0.10.2"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676"; }; + features = builtins.concatLists [ + [ "default" ] + [ "std" ] + ]; + dependencies = { + cfg_if = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }; + ${ if hostPlatform.parsed.cpu.name == "aarch64" || hostPlatform.parsed.cpu.name == "x86_64" || hostPlatform.parsed.cpu.name == "i686" then "cpufeatures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cpufeatures."0.2.2" { inherit profileName; }; + digest = rustPackages."registry+https://github.com/rust-lang/crates.io-index".digest."0.10.3" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".shlex."1.1.0" = overridableMkRustCrate (profileName: rec { name = "shlex"; version = "1.1.0"; @@ -5555,9 +5645,9 @@ in ]; dependencies = { ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-uwp-windows-gnu" || hostPlatform.config == "i686-pc-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); From 0176da3ad2aae9d18cb04feb452e0243cfb940fc Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 22 Jul 2022 18:37:20 +0200 Subject: [PATCH 043/149] Make clippy happy --- src/util/async_hash.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/util/async_hash.rs b/src/util/async_hash.rs index be0535de..fa8ee7ff 100644 --- a/src/util/async_hash.rs +++ b/src/util/async_hash.rs @@ -56,3 +56,9 @@ impl AsyncHasher { self.task.await.unwrap() } } + +impl Default for AsyncHasher { + fn default() -> Self { + Self::new() + } +} From 2cad656a0332b19481ce779f5026b07c6ed8198f Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 22 Jul 2022 18:40:06 +0200 Subject: [PATCH 044/149] More make clippy happy --- src/api/s3/put.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/api/s3/put.rs b/src/api/s3/put.rs index a182f04d..2c51909f 100644 --- a/src/api/s3/put.rs +++ b/src/api/s3/put.rs @@ -286,8 +286,6 @@ async fn read_and_put_blocks> + Unpin>( ) -> Result<(u64, GenericArray, Hash), Error> { let tracer = opentelemetry::global::tracer("garage"); - let first_block = Bytes::from(first_block); - let md5hasher = AsyncHasher::::new(); let sha256hasher = AsyncHasher::::new(); From 381eb9a5a1dc530ce864ac2b3dc8eb0d454f1bc9 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 22 Jul 2022 18:55:52 +0200 Subject: [PATCH 045/149] Fix tests --- Cargo.lock | 22 +--------------------- src/garage/Cargo.toml | 2 +- src/garage/tests/lib.rs | 3 +++ 3 files changed, 5 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 260ae907..d54cabd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -618,16 +618,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "crypto-mac" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" -dependencies = [ - "generic-array", - "subtle", -] - [[package]] name = "crypto-mac" version = "0.11.1" @@ -1006,7 +996,7 @@ dependencies = [ "garage_util 0.7.0", "garage_web", "hex", - "hmac 0.10.1", + "hmac 0.12.1", "http", "hyper", "kuska-sodiumoxide", @@ -1510,16 +1500,6 @@ dependencies = [ "itertools 0.4.19", ] -[[package]] -name = "hmac" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" -dependencies = [ - "crypto-mac 0.10.1", - "digest 0.9.0", -] - [[package]] name = "hmac" version = "0.11.0" diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index 80802a16..2cb8ec46 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -63,7 +63,7 @@ prometheus = "0.13" aws-sdk-s3 = "0.8" chrono = "0.4" http = "0.2" -hmac = "0.10" +hmac = "0.12" hyper = { version = "0.14", features = ["client", "http1", "runtime"] } sha2 = "0.10" diff --git a/src/garage/tests/lib.rs b/src/garage/tests/lib.rs index 0106ad10..99aa1d58 100644 --- a/src/garage/tests/lib.rs +++ b/src/garage/tests/lib.rs @@ -3,5 +3,8 @@ mod common; mod admin; mod bucket; + +#[cfg(feature="k2v")] mod k2v; + mod s3; From ff4771c36ac06761a50364d53dc65f65ca6750f9 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 22 Jul 2022 18:56:53 +0200 Subject: [PATCH 046/149] cargo fmt --- src/garage/tests/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/garage/tests/lib.rs b/src/garage/tests/lib.rs index 99aa1d58..24d794c3 100644 --- a/src/garage/tests/lib.rs +++ b/src/garage/tests/lib.rs @@ -4,7 +4,7 @@ mod common; mod admin; mod bucket; -#[cfg(feature="k2v")] +#[cfg(feature = "k2v")] mod k2v; mod s3; From 49154a78d88b54cec8b23bc5e54dfabff88af780 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 22 Jul 2022 18:57:40 +0200 Subject: [PATCH 047/149] Update cargo.nix --- Cargo.nix | 36 +++++++----------------------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/Cargo.nix b/Cargo.nix index cf86ff0e..144967ee 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -759,7 +759,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.config == "aarch64-linux-android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -886,17 +886,6 @@ in }; }); - "registry+https://github.com/rust-lang/crates.io-index".crypto-mac."0.10.1" = overridableMkRustCrate (profileName: rec { - name = "crypto-mac"; - version = "0.10.1"; - registry = "registry+https://github.com/rust-lang/crates.io-index"; - src = fetchCratesIo { inherit name version; sha256 = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a"; }; - dependencies = { - generic_array = rustPackages."registry+https://github.com/rust-lang/crates.io-index".generic-array."0.14.5" { inherit profileName; }; - subtle = rustPackages."registry+https://github.com/rust-lang/crates.io-index".subtle."2.4.1" { inherit profileName; }; - }; - }); - "registry+https://github.com/rust-lang/crates.io-index".crypto-mac."0.11.1" = overridableMkRustCrate (profileName: rec { name = "crypto-mac"; version = "0.11.1"; @@ -1449,7 +1438,7 @@ in aws_sdk_s3 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-sdk-s3."0.8.0" { inherit profileName; }; base64 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.13.0" { inherit profileName; }; chrono = rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.19" { inherit profileName; }; - hmac = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hmac."0.10.1" { inherit profileName; }; + hmac = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hmac."0.12.1" { inherit profileName; }; http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; hyper = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; @@ -2046,17 +2035,6 @@ in }; }); - "registry+https://github.com/rust-lang/crates.io-index".hmac."0.10.1" = overridableMkRustCrate (profileName: rec { - name = "hmac"; - version = "0.10.1"; - registry = "registry+https://github.com/rust-lang/crates.io-index"; - src = fetchCratesIo { inherit name version; sha256 = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"; }; - dependencies = { - crypto_mac = rustPackages."registry+https://github.com/rust-lang/crates.io-index".crypto-mac."0.10.1" { inherit profileName; }; - digest = rustPackages."registry+https://github.com/rust-lang/crates.io-index".digest."0.9.0" { inherit profileName; }; - }; - }); - "registry+https://github.com/rust-lang/crates.io-index".hmac."0.11.0" = overridableMkRustCrate (profileName: rec { name = "hmac"; version = "0.11.0"; @@ -2830,7 +2808,7 @@ in [ "os-poll" ] ]; dependencies = { - ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "wasi" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "wasi" || hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if hostPlatform.isWindows then "miow" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".miow."0.3.7" { inherit profileName; }; ${ if hostPlatform.isWindows then "ntapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ntapi."0.3.7" { inherit profileName; }; @@ -4012,7 +3990,7 @@ in ]; dependencies = { ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" || hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "i686" || hostPlatform.parsed.cpu.name == "x86_64" || (hostPlatform.parsed.cpu.name == "aarch64" || hostPlatform.parsed.cpu.name == "armv6l" || hostPlatform.parsed.cpu.name == "armv7l") && (hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "fuchsia" || hostPlatform.parsed.kernel.name == "linux") then "spin" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".spin."0.5.2" { inherit profileName; }; untrusted = rustPackages."registry+https://github.com/rust-lang/crates.io-index".untrusted."0.7.1" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "wasm32" && hostPlatform.parsed.vendor.name == "unknown" && hostPlatform.parsed.kernel.name == "unknown" && hostPlatform.parsed.abi.name == "" then "web_sys" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.56" { inherit profileName; }; @@ -5645,9 +5623,9 @@ in ]; dependencies = { ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-uwp-windows-gnu" || hostPlatform.config == "i686-pc-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); From ad35b18bb146fcbf5e817c10837c6e835b1af5b7 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 25 Jul 2022 11:59:55 +0200 Subject: [PATCH 048/149] Faster chunker --- src/api/s3/put.rs | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/api/s3/put.rs b/src/api/s3/put.rs index 2c51909f..e6698bfa 100644 --- a/src/api/s3/put.rs +++ b/src/api/s3/put.rs @@ -387,7 +387,8 @@ struct StreamChunker>> { stream: S, read_all: bool, block_size: usize, - buf: VecDeque, + buf: VecDeque, + buf_len: usize, } impl> + Unpin> StreamChunker { @@ -396,29 +397,50 @@ impl> + Unpin> StreamChunker { stream, read_all: false, block_size, - buf: VecDeque::with_capacity(2 * block_size), + buf: VecDeque::with_capacity(8), + buf_len: 0, } } async fn next(&mut self) -> Result>, Error> { - while !self.read_all && self.buf.len() < self.block_size { + while !self.read_all && self.buf_len < self.block_size { if let Some(block) = self.stream.next().await { let bytes = block?; trace!("Body next: {} bytes", bytes.len()); - self.buf.extend(bytes); + self.buf_len += bytes.len(); + self.buf.push_back(bytes); } else { self.read_all = true; } } - if self.buf.is_empty() { + if self.buf_len == 0 { Ok(None) - } else if self.buf.len() <= self.block_size { - let block = self.buf.drain(..).collect::>(); - Ok(Some(block)) } else { - let block = self.buf.drain(..self.block_size).collect::>(); - Ok(Some(block)) + let mut slices = Vec::with_capacity(self.buf.len()); + let mut taken = 0; + while self.buf_len > 0 && taken < self.block_size { + let front = self.buf.pop_front().unwrap(); + if taken + front.len() <= self.block_size { + taken += front.len(); + self.buf_len -= front.len(); + slices.push(front); + } else { + let front_take = self.block_size - taken; + slices.push(front.slice(..front_take)); + self.buf.push_front(front.slice(front_take..)); + self.buf_len -= front_take; + break; + } + } + Ok(Some( + slices + .iter() + .map(|x| &x[..]) + .collect::>() + .concat() + .into(), + )) } } } From 16f6a1a65d4b973ea13cd00bbfdd7e225041e447 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 25 Jul 2022 12:06:06 +0200 Subject: [PATCH 049/149] fix clippy --- src/api/s3/put.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/api/s3/put.rs b/src/api/s3/put.rs index e6698bfa..dc0530df 100644 --- a/src/api/s3/put.rs +++ b/src/api/s3/put.rs @@ -434,12 +434,7 @@ impl> + Unpin> StreamChunker { } } Ok(Some( - slices - .iter() - .map(|x| &x[..]) - .collect::>() - .concat() - .into(), + slices.iter().map(|x| &x[..]).collect::>().concat(), )) } } From 8e7e680afe39f48fe15f365c9ef3fee57596e119 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 22 Jul 2022 15:20:00 +0200 Subject: [PATCH 050/149] First adaptation to WIP netapp with streaming body --- Cargo.lock | 74 ++++++++++++++++++++-------------------- src/block/manager.rs | 19 +++++------ src/garage/Cargo.toml | 5 ++- src/garage/admin.rs | 4 +-- src/garage/cli/cmd.rs | 6 ++-- src/garage/cli/layout.rs | 6 ++-- src/model/Cargo.toml | 5 ++- src/rpc/Cargo.toml | 5 ++- src/rpc/rpc_helper.rs | 71 ++++++++++++++++---------------------- src/rpc/system.rs | 7 ++-- src/table/schema.rs | 2 +- src/util/Cargo.toml | 5 ++- 12 files changed, 97 insertions(+), 112 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d54cabd0..8edffc6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -154,7 +154,7 @@ dependencies = [ "aws-smithy-types", "aws-smithy-xml", "aws-types", - "bytes 1.1.0", + "bytes 1.2.0", "http", "md5", "tokio-stream", @@ -184,7 +184,7 @@ checksum = "51d371fb688d909e5b866ff1f297bbec4621eed4f9fcdac566fcc33541f0c6a6" dependencies = [ "aws-smithy-eventstream", "aws-smithy-http", - "bytes 1.1.0", + "bytes 1.2.0", "form_urlencoded", "hex", "http", @@ -218,7 +218,7 @@ dependencies = [ "aws-smithy-http", "aws-smithy-http-tower", "aws-smithy-types", - "bytes 1.1.0", + "bytes 1.2.0", "fastrand", "http", "http-body", @@ -239,7 +239,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f972226c639e0dc1eca2cb0220c1b5799e2bfc62eda37845b662c5d0cb972371" dependencies = [ "aws-smithy-types", - "bytes 1.1.0", + "bytes 1.2.0", "crc32fast", ] @@ -251,7 +251,7 @@ checksum = "12c787e24b757634453a60ff05948aa1b450f5b3a7a2094f22acff8a5022635b" dependencies = [ "aws-smithy-eventstream", "aws-smithy-types", - "bytes 1.1.0", + "bytes 1.2.0", "bytes-utils", "futures-core", "http", @@ -271,7 +271,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64f80a2c56fc09fc9a2da3c63f286ec2a89465433219f8165e14e522283a5eb8" dependencies = [ "aws-smithy-http", - "bytes 1.1.0", + "bytes 1.2.0", "http", "http-body", "pin-project 1.0.10", @@ -391,9 +391,9 @@ checksum = "e0dcbc35f504eb6fc275a6d20e4ebcda18cf50d40ba6fabff8c711fa16cb3b16" [[package]] name = "bytes" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e" [[package]] name = "bytes-utils" @@ -401,7 +401,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1934a3ef9cac8efde4966a92781e77713e1ba329f1d42e446c7d7eba340d8ef1" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.0", "either", ] @@ -982,7 +982,7 @@ dependencies = [ "async-trait", "aws-sdk-s3", "base64", - "bytes 1.1.0", + "bytes 1.2.0", "bytesize", "chrono", "futures", @@ -1026,7 +1026,7 @@ version = "0.7.0" dependencies = [ "async-trait", "base64", - "bytes 1.1.0", + "bytes 1.2.0", "chrono", "crypto-common", "err-derive 0.3.1", @@ -1071,7 +1071,7 @@ version = "0.7.0" dependencies = [ "arc-swap", "async-trait", - "bytes 1.1.0", + "bytes 1.2.0", "futures", "futures-util", "garage_db", @@ -1166,7 +1166,7 @@ checksum = "81e693aa4582cfe7a7ce70c07880e3662544b5d0cd68bc4b59c53febfbb8d1ec" dependencies = [ "arc-swap", "async-trait", - "bytes 1.1.0", + "bytes 1.2.0", "futures", "futures-util", "garage_util 0.5.1", @@ -1191,7 +1191,7 @@ version = "0.7.0" dependencies = [ "arc-swap", "async-trait", - "bytes 1.1.0", + "bytes 1.2.0", "futures", "futures-util", "garage_util 0.7.0", @@ -1224,7 +1224,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c3557f3757e2acd29eaee86804d4e6c38d2abda81b4b349d8a0d2277044265c" dependencies = [ "async-trait", - "bytes 1.1.0", + "bytes 1.2.0", "futures", "futures-util", "garage_rpc 0.5.1", @@ -1244,7 +1244,7 @@ name = "garage_table" version = "0.7.0" dependencies = [ "async-trait", - "bytes 1.1.0", + "bytes 1.2.0", "futures", "futures-util", "garage_db", @@ -1390,7 +1390,7 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62eeb471aa3e3c9197aa4bfeabfe02982f6dc96f750486c0bb0009ac58b26d2b" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.0", "fnv", "futures-core", "futures-sink", @@ -1525,7 +1525,7 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.0", "fnv", "itoa", ] @@ -1536,7 +1536,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.0", "http", "pin-project-lite", ] @@ -1580,7 +1580,7 @@ version = "0.14.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.0", "futures-channel", "futures-core", "futures-util", @@ -1633,7 +1633,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.0", "hyper", "native-tls", "tokio", @@ -1781,7 +1781,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f8de9873b904e74b3533f77493731ee26742418077503683db44e1b3c54aa5c" dependencies = [ "base64", - "bytes 1.1.0", + "bytes 1.2.0", "chrono", "http", "percent-encoding", @@ -1810,7 +1810,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd12d68768b54bbd50547f4c7b57b73cff680ef8da3ba409463ee995cf0d707" dependencies = [ "base64", - "bytes 1.1.0", + "bytes 1.2.0", "chrono", "dirs-next", "either", @@ -2081,7 +2081,7 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f8f35e687561d5c1667590911e6698a8cb714a134a7505718a182e7bc9d3836" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.0", "encoding_rs", "futures-util", "http", @@ -2142,12 +2142,11 @@ dependencies = [ [[package]] name = "netapp" version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6419a4b836774192e13fedb05c0e5f414ee8df9ca0c467456a0bacde06c29ee" +source = "git+https://git.deuxfleurs.fr/lx/netapp?branch=stream-body#cbc21e40acfc420a3e452a1fd488c6a96694b0f2" dependencies = [ "arc-swap", "async-trait", - "bytes 0.6.0", + "bytes 1.2.0", "cfg-if 1.0.0", "err-derive 0.2.4", "futures", @@ -2157,6 +2156,7 @@ dependencies = [ "log", "opentelemetry", "opentelemetry-contrib", + "pin-project 1.0.10", "rand 0.5.6", "rmp-serde 0.14.4", "serde", @@ -2640,7 +2640,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.0", "prost-derive", ] @@ -2650,7 +2650,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.0", "heck 0.3.3", "itertools 0.10.3", "lazy_static", @@ -2683,7 +2683,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.0", "prost", ] @@ -2986,7 +2986,7 @@ checksum = "1db30db44ea73551326269adcf7a2169428a054f14faf9e1768f2163494f2fa2" dependencies = [ "async-trait", "base64", - "bytes 1.1.0", + "bytes 1.2.0", "crc32fast", "futures", "http", @@ -3028,7 +3028,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ae95491c8b4847931e291b151127eccd6ff8ca13f33603eb3d0035ecb05272" dependencies = [ "base64", - "bytes 1.1.0", + "bytes 1.2.0", "chrono", "digest 0.9.0", "futures", @@ -3594,7 +3594,7 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.0", "libc", "memchr", "mio", @@ -3667,7 +3667,7 @@ version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.0", "futures-core", "futures-io", "futures-sink", @@ -3683,7 +3683,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64910e1b9c1901aaf5375561e35b9c057d95ff41a44ede043a03e09279eabaf1" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.0", "futures-core", "futures-sink", "log", @@ -3709,7 +3709,7 @@ dependencies = [ "async-stream", "async-trait", "base64", - "bytes 1.1.0", + "bytes 1.2.0", "futures-core", "futures-util", "h2", @@ -3770,7 +3770,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81eca72647e58054bbfa41e6f297c23436f1c60aff6e5eb38455a0f9ca420bb5" dependencies = [ "base64", - "bytes 1.1.0", + "bytes 1.2.0", "futures-core", "futures-util", "http", diff --git a/src/block/manager.rs b/src/block/manager.rs index be53ec6e..408de148 100644 --- a/src/block/manager.rs +++ b/src/block/manager.rs @@ -8,7 +8,6 @@ use async_trait::async_trait; use bytes::Bytes; use serde::{Deserialize, Serialize}; -use futures::future::*; use tokio::fs; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::select; @@ -637,24 +636,24 @@ impl BlockManager { } who.retain(|id| *id != self.system.id); - let msg = Arc::new(BlockRpc::NeedBlockQuery(*hash)); - let who_needs_fut = who.iter().map(|to| { - self.system.rpc.call_arc( + let who_needs_resps = self + .system + .rpc + .call_many( &self.endpoint, - *to, - msg.clone(), + &who, + BlockRpc::NeedBlockQuery(*hash), RequestStrategy::with_priority(PRIO_BACKGROUND) .with_timeout(NEED_BLOCK_QUERY_TIMEOUT), ) - }); - let who_needs_resps = join_all(who_needs_fut).await; + .await?; let mut need_nodes = vec![]; - for (node, needed) in who.iter().zip(who_needs_resps.into_iter()) { + for (node, needed) in who_needs_resps.into_iter() { match needed.err_context("NeedBlockQuery RPC")? { BlockRpc::NeedBlockReply(needed) => { if needed { - need_nodes.push(*node); + need_nodes.push(node); } } m => { diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index 2cb8ec46..5a872c7a 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -50,9 +50,8 @@ futures = "0.3" futures-util = "0.3" tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] } -#netapp = { version = "0.3.2", git = "https://git.deuxfleurs.fr/lx/netapp" } -#netapp = { version = "0.4", path = "../../../netapp" } -netapp = "0.4" +#netapp = "0.4" +netapp = { version = "0.4.4", git = "https://git.deuxfleurs.fr/lx/netapp", branch = "stream-body", features = ["telemetry"] } opentelemetry = { version = "0.17", features = [ "rt-tokio" ] } opentelemetry-prometheus = "0.10" diff --git a/src/garage/admin.rs b/src/garage/admin.rs index 71ee608c..64a448fc 100644 --- a/src/garage/admin.rs +++ b/src/garage/admin.rs @@ -681,7 +681,7 @@ impl AdminRpcHandler { .endpoint .call( &node, - &AdminRpc::LaunchRepair(opt_to_send.clone()), + AdminRpc::LaunchRepair(opt_to_send.clone()), PRIO_NORMAL, ) .await; @@ -721,7 +721,7 @@ impl AdminRpcHandler { let node_id = (*node).into(); match self .endpoint - .call(&node_id, &AdminRpc::Stats(opt), PRIO_NORMAL) + .call(&node_id, AdminRpc::Stats(opt), PRIO_NORMAL) .await? { Ok(AdminRpc::Ok(s)) => writeln!(&mut ret, "{}", s).unwrap(), diff --git a/src/garage/cli/cmd.rs b/src/garage/cli/cmd.rs index 1aa2c2ff..c8b96489 100644 --- a/src/garage/cli/cmd.rs +++ b/src/garage/cli/cmd.rs @@ -47,7 +47,7 @@ pub async fn cli_command_dispatch( pub async fn cmd_status(rpc_cli: &Endpoint, rpc_host: NodeID) -> Result<(), Error> { let status = match rpc_cli - .call(&rpc_host, &SystemRpc::GetKnownNodes, PRIO_NORMAL) + .call(&rpc_host, SystemRpc::GetKnownNodes, PRIO_NORMAL) .await?? { SystemRpc::ReturnKnownNodes(nodes) => nodes, @@ -149,7 +149,7 @@ pub async fn cmd_connect( args: ConnectNodeOpt, ) -> Result<(), Error> { match rpc_cli - .call(&rpc_host, &SystemRpc::Connect(args.node), PRIO_NORMAL) + .call(&rpc_host, SystemRpc::Connect(args.node), PRIO_NORMAL) .await?? { SystemRpc::Ok => { @@ -165,7 +165,7 @@ pub async fn cmd_admin( rpc_host: NodeID, args: AdminRpc, ) -> Result<(), HelperError> { - match rpc_cli.call(&rpc_host, &args, PRIO_NORMAL).await?? { + match rpc_cli.call(&rpc_host, args, PRIO_NORMAL).await?? { AdminRpc::Ok(msg) => { println!("{}", msg); } diff --git a/src/garage/cli/layout.rs b/src/garage/cli/layout.rs index db0af57c..3884bb92 100644 --- a/src/garage/cli/layout.rs +++ b/src/garage/cli/layout.rs @@ -36,7 +36,7 @@ pub async fn cmd_assign_role( args: AssignRoleOpt, ) -> Result<(), Error> { let status = match rpc_cli - .call(&rpc_host, &SystemRpc::GetKnownNodes, PRIO_NORMAL) + .call(&rpc_host, SystemRpc::GetKnownNodes, PRIO_NORMAL) .await?? { SystemRpc::ReturnKnownNodes(nodes) => nodes, @@ -245,7 +245,7 @@ pub async fn fetch_layout( rpc_host: NodeID, ) -> Result { match rpc_cli - .call(&rpc_host, &SystemRpc::PullClusterLayout, PRIO_NORMAL) + .call(&rpc_host, SystemRpc::PullClusterLayout, PRIO_NORMAL) .await?? { SystemRpc::AdvertiseClusterLayout(t) => Ok(t), @@ -261,7 +261,7 @@ pub async fn send_layout( rpc_cli .call( &rpc_host, - &SystemRpc::AdvertiseClusterLayout(layout), + SystemRpc::AdvertiseClusterLayout(layout), PRIO_NORMAL, ) .await??; diff --git a/src/model/Cargo.toml b/src/model/Cargo.toml index d908dc01..a97bce4d 100644 --- a/src/model/Cargo.toml +++ b/src/model/Cargo.toml @@ -40,9 +40,8 @@ futures-util = "0.3" tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] } opentelemetry = "0.17" -#netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" } -#netapp = { version = "0.4", path = "../../../netapp" } -netapp = "0.4" +#netapp = "0.4" +netapp = { version = "0.4.4", git = "https://git.deuxfleurs.fr/lx/netapp", branch = "stream-body", features = ["telemetry"] } [features] k2v = [ "garage_util/k2v" ] diff --git a/src/rpc/Cargo.toml b/src/rpc/Cargo.toml index 73328993..5d5151cd 100644 --- a/src/rpc/Cargo.toml +++ b/src/rpc/Cargo.toml @@ -46,9 +46,8 @@ tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi tokio-stream = { version = "0.1", features = ["net"] } opentelemetry = "0.17" -#netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" } -#netapp = { version = "0.4", path = "../../../netapp", features = ["telemetry"] } -netapp = { version = "0.4.4", features = ["telemetry"] } +#netapp = { version = "0.4.4", features = ["telemetry"] } +netapp = { version = "0.4.4", git = "https://git.deuxfleurs.fr/lx/netapp", branch = "stream-body", features = ["telemetry"] } hyper = { version = "0.14", features = ["client", "http1", "runtime", "tcp"] } diff --git a/src/rpc/rpc_helper.rs b/src/rpc/rpc_helper.rs index 34717d3b..079cdc70 100644 --- a/src/rpc/rpc_helper.rs +++ b/src/rpc/rpc_helper.rs @@ -15,9 +15,9 @@ use opentelemetry::{ Context, }; -pub use netapp::endpoint::{Endpoint, EndpointHandler, Message as Rpc}; +pub use netapp::endpoint::{Endpoint, EndpointHandler}; +pub use netapp::message::{Message as Rpc, *}; use netapp::peering::fullmesh::FullMeshPeeringStrategy; -pub use netapp::proto::*; pub use netapp::{NetApp, NodeID}; use garage_util::background::BackgroundRunner; @@ -30,10 +30,8 @@ use crate::ring::Ring; const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); -// Try to never have more than 200MB of outgoing requests -// buffered at the same time. Other requests are queued until -// space is freed. -const REQUEST_BUFFER_SIZE: usize = 200 * 1024 * 1024; +// Don't allow more than 100 concurrent outgoing RPCs. +const MAX_CONCURRENT_REQUESTS: usize = 100; /// Strategy to apply when making RPC #[derive(Copy, Clone)] @@ -95,7 +93,7 @@ impl RpcHelper { background: Arc, ring: watch::Receiver>, ) -> Self { - let sem = Arc::new(Semaphore::new(REQUEST_BUFFER_SIZE)); + let sem = Arc::new(Semaphore::new(MAX_CONCURRENT_REQUESTS)); let metrics = RpcMetrics::new(sem.clone()); @@ -109,29 +107,16 @@ impl RpcHelper { })) } - pub async fn call( + pub async fn call( &self, endpoint: &Endpoint, to: Uuid, - msg: M, - strat: RequestStrategy, - ) -> Result - where - M: Rpc>, - H: EndpointHandler, - { - self.call_arc(endpoint, to, Arc::new(msg), strat).await - } - - pub async fn call_arc( - &self, - endpoint: &Endpoint, - to: Uuid, - msg: Arc, + msg: N, strat: RequestStrategy, ) -> Result where M: Rpc>, + N: IntoReq + Send, H: EndpointHandler, { let metric_tags = [ @@ -140,11 +125,10 @@ impl RpcHelper { KeyValue::new("to", format!("{:?}", to)), ]; - let msg_size = rmp_to_vec_all_named(&msg)?.len() as u32; let permit = self .0 .request_buffer_semaphore - .acquire_many(msg_size) + .acquire() .record_duration(&self.0.metrics.rpc_queueing_time, &metric_tags) .await?; @@ -152,7 +136,7 @@ impl RpcHelper { let node_id = to.into(); let rpc_call = endpoint - .call(&node_id, msg, strat.rs_priority) + .call_streaming(&node_id, msg, strat.rs_priority) .record_duration(&self.0.metrics.rpc_duration, &metric_tags); select! { @@ -162,7 +146,7 @@ impl RpcHelper { if res.is_err() { self.0.metrics.rpc_netapp_error_counter.add(1, &metric_tags); } - let res = res?; + let res = res?.into_msg(); if res.is_err() { self.0.metrics.rpc_garage_error_counter.add(1, &metric_tags); @@ -178,37 +162,41 @@ impl RpcHelper { } } - pub async fn call_many( + pub async fn call_many( &self, endpoint: &Endpoint, to: &[Uuid], - msg: M, + msg: N, strat: RequestStrategy, - ) -> Vec<(Uuid, Result)> + ) -> Result)>, Error> where M: Rpc>, + N: IntoReq, H: EndpointHandler, { - let msg = Arc::new(msg); + let msg = msg.into_req().map_err(netapp::error::Error::from)?; + let resps = join_all( to.iter() - .map(|to| self.call_arc(endpoint, *to, msg.clone(), strat)), + .map(|to| self.call(endpoint, *to, msg.clone(), strat)), ) .await; - to.iter() + Ok(to + .iter() .cloned() .zip(resps.into_iter()) - .collect::>() + .collect::>()) } - pub async fn broadcast( + pub async fn broadcast( &self, endpoint: &Endpoint, - msg: M, + msg: N, strat: RequestStrategy, - ) -> Vec<(Uuid, Result)> + ) -> Result)>, Error> where M: Rpc>, + N: IntoReq, H: EndpointHandler, { let to = self @@ -262,20 +250,21 @@ impl RpcHelper { .await } - async fn try_call_many_internal( + async fn try_call_many_internal( &self, endpoint: &Arc>, to: &[Uuid], - msg: M, + msg: N, strategy: RequestStrategy, quorum: usize, ) -> Result, Error> where M: Rpc> + 'static, + N: IntoReq, H: EndpointHandler + 'static, S: Send + 'static, { - let msg = Arc::new(msg); + let msg = msg.into_req().map_err(netapp::error::Error::from)?; // Build future for each request // They are not started now: they are added below in a FuturesUnordered @@ -285,7 +274,7 @@ impl RpcHelper { let msg = msg.clone(); let endpoint2 = endpoint.clone(); (to, async move { - self2.call_arc(&endpoint2, to, msg, strategy).await + self2.call(&endpoint2, to, msg, strategy).await }) }); diff --git a/src/rpc/system.rs b/src/rpc/system.rs index f9f2970b..04ef2f69 100644 --- a/src/rpc/system.rs +++ b/src/rpc/system.rs @@ -16,8 +16,8 @@ use tokio::sync::watch; use tokio::sync::Mutex; use netapp::endpoint::{Endpoint, EndpointHandler}; +use netapp::message::*; use netapp::peering::fullmesh::FullMeshPeeringStrategy; -use netapp::proto::*; use netapp::util::parse_and_resolve_peer_addr; use netapp::{NetApp, NetworkKey, NodeID, NodeKey}; @@ -544,7 +544,7 @@ impl System { SystemRpc::AdvertiseClusterLayout(layout), RequestStrategy::with_priority(PRIO_HIGH), ) - .await; + .await?; Ok(()) }); self.background.spawn(self.clone().save_cluster_layout()); @@ -559,7 +559,8 @@ impl System { self.update_local_status(); let local_status: NodeStatus = self.local_status.load().as_ref().clone(); - self.rpc + let _ = self + .rpc .broadcast( &self.system_endpoint, SystemRpc::AdvertiseStatus(local_status), diff --git a/src/table/schema.rs b/src/table/schema.rs index 74f57798..f37e98d8 100644 --- a/src/table/schema.rs +++ b/src/table/schema.rs @@ -60,7 +60,7 @@ pub trait Entry: } /// Trait for the schema used in a table -pub trait TableSchema: Send + Sync { +pub trait TableSchema: Send + Sync + 'static { /// The name of the table in the database const TABLE_NAME: &'static str; diff --git a/src/util/Cargo.toml b/src/util/Cargo.toml index 7d79f21a..89064592 100644 --- a/src/util/Cargo.toml +++ b/src/util/Cargo.toml @@ -36,9 +36,8 @@ toml = "0.5" futures = "0.3" tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] } -#netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" } -#netapp = { version = "0.4", path = "../../../netapp" } -netapp = "0.4" +#netapp = "0.4" +netapp = { version = "0.4.4", git = "https://git.deuxfleurs.fr/lx/netapp", branch = "stream-body", features = ["telemetry"] } http = "0.2" hyper = "0.14" From a35d4da721db3550a2833d8576d4283bc999e8df Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 22 Jul 2022 16:45:45 +0200 Subject: [PATCH 051/149] update netapp to 0.5 --- Cargo.lock | 12 ++++++------ src/garage/Cargo.toml | 2 +- src/model/Cargo.toml | 2 +- src/rpc/Cargo.toml | 2 +- src/util/Cargo.toml | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8edffc6a..ef72f911 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1000,7 +1000,7 @@ dependencies = [ "http", "hyper", "kuska-sodiumoxide", - "netapp 0.4.4", + "netapp 0.5.0", "opentelemetry", "opentelemetry-otlp", "opentelemetry-prometheus", @@ -1147,7 +1147,7 @@ dependencies = [ "garage_table 0.7.0", "garage_util 0.7.0", "hex", - "netapp 0.4.4", + "netapp 0.5.0", "opentelemetry", "rand 0.8.5", "rmp-serde 0.15.5", @@ -1202,7 +1202,7 @@ dependencies = [ "k8s-openapi", "kube", "kuska-sodiumoxide", - "netapp 0.4.4", + "netapp 0.5.0", "openssl", "opentelemetry", "pnet_datalink", @@ -1301,7 +1301,7 @@ dependencies = [ "hex", "http", "hyper", - "netapp 0.4.4", + "netapp 0.5.0", "opentelemetry", "rand 0.8.5", "rmp-serde 0.15.5", @@ -2141,8 +2141,8 @@ dependencies = [ [[package]] name = "netapp" -version = "0.4.4" -source = "git+https://git.deuxfleurs.fr/lx/netapp?branch=stream-body#cbc21e40acfc420a3e452a1fd488c6a96694b0f2" +version = "0.5.0" +source = "git+https://git.deuxfleurs.fr/lx/netapp?branch=stream-body#ab80ade4f0034cbdcf15a99c674730f85eb06870" dependencies = [ "arc-swap", "async-trait", diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index 5a872c7a..1e96f1f3 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -51,7 +51,7 @@ futures-util = "0.3" tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] } #netapp = "0.4" -netapp = { version = "0.4.4", git = "https://git.deuxfleurs.fr/lx/netapp", branch = "stream-body", features = ["telemetry"] } +netapp = { version = "0.5", git = "https://git.deuxfleurs.fr/lx/netapp", branch = "stream-body", features = ["telemetry"] } opentelemetry = { version = "0.17", features = [ "rt-tokio" ] } opentelemetry-prometheus = "0.10" diff --git a/src/model/Cargo.toml b/src/model/Cargo.toml index a97bce4d..73011e0d 100644 --- a/src/model/Cargo.toml +++ b/src/model/Cargo.toml @@ -41,7 +41,7 @@ tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi opentelemetry = "0.17" #netapp = "0.4" -netapp = { version = "0.4.4", git = "https://git.deuxfleurs.fr/lx/netapp", branch = "stream-body", features = ["telemetry"] } +netapp = { version = "0.5", git = "https://git.deuxfleurs.fr/lx/netapp", branch = "stream-body", features = ["telemetry"] } [features] k2v = [ "garage_util/k2v" ] diff --git a/src/rpc/Cargo.toml b/src/rpc/Cargo.toml index 5d5151cd..5986b7bf 100644 --- a/src/rpc/Cargo.toml +++ b/src/rpc/Cargo.toml @@ -47,7 +47,7 @@ tokio-stream = { version = "0.1", features = ["net"] } opentelemetry = "0.17" #netapp = { version = "0.4.4", features = ["telemetry"] } -netapp = { version = "0.4.4", git = "https://git.deuxfleurs.fr/lx/netapp", branch = "stream-body", features = ["telemetry"] } +netapp = { version = "0.5.0", git = "https://git.deuxfleurs.fr/lx/netapp", branch = "stream-body", features = ["telemetry"] } hyper = { version = "0.14", features = ["client", "http1", "runtime", "tcp"] } diff --git a/src/util/Cargo.toml b/src/util/Cargo.toml index 89064592..a70f68b9 100644 --- a/src/util/Cargo.toml +++ b/src/util/Cargo.toml @@ -37,7 +37,7 @@ futures = "0.3" tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] } #netapp = "0.4" -netapp = { version = "0.4.4", git = "https://git.deuxfleurs.fr/lx/netapp", branch = "stream-body", features = ["telemetry"] } +netapp = { version = "0.5", git = "https://git.deuxfleurs.fr/lx/netapp", branch = "stream-body", features = ["telemetry"] } http = "0.2" hyper = "0.14" From 605a630333c8ee60c55fe011a375c01277bba173 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 22 Jul 2022 18:20:27 +0200 Subject: [PATCH 052/149] Use streaming in block manager --- Cargo.lock | 18 ++- src/api/s3/copy.rs | 12 +- src/api/s3/get.rs | 29 +++-- src/block/Cargo.toml | 3 + src/block/block.rs | 37 +++++-- src/block/manager.rs | 249 ++++++++++++++++++++++++++++++++---------- src/rpc/rpc_helper.rs | 24 ++-- 7 files changed, 284 insertions(+), 88 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef72f911..9d12f523 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,6 +50,20 @@ dependencies = [ "serde_json", ] +[[package]] +name = "async-compression" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00461f243d703f6999c8e7494f077799f1362720a55ae49a90ffe6214032fc0b" +dependencies = [ + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "zstd", + "zstd-safe", +] + [[package]] name = "async-stream" version = "0.3.3" @@ -1070,6 +1084,7 @@ name = "garage_block" version = "0.7.0" dependencies = [ "arc-swap", + "async-compression", "async-trait", "bytes 1.2.0", "futures", @@ -1085,6 +1100,7 @@ dependencies = [ "serde", "serde_bytes", "tokio", + "tokio-util 0.6.9", "tracing", "zstd", ] @@ -1292,7 +1308,7 @@ version = "0.7.0" dependencies = [ "async-trait", "blake2", - "bytes 1.1.0", + "bytes 1.2.0", "chrono", "digest 0.10.3", "err-derive 0.3.1", diff --git a/src/api/s3/copy.rs b/src/api/s3/copy.rs index 4415a037..54a565e0 100644 --- a/src/api/s3/copy.rs +++ b/src/api/s3/copy.rs @@ -5,6 +5,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use futures::{stream, stream::Stream, StreamExt, TryFutureExt}; use md5::{Digest as Md5Digest, Md5}; +use bytes::Bytes; use hyper::{Body, Request, Response}; use serde::Serialize; @@ -311,7 +312,7 @@ pub async fn handle_upload_part_copy( stream::once(async move { let data = garage3.block_manager.rpc_get_block(&block_hash).await?; match range_to_copy { - Some(r) => Ok((data[r].to_vec(), None)), + Some(r) => Ok((data.slice(r), None)), None => Ok((data, Some(block_hash))), } }) @@ -556,7 +557,7 @@ impl CopyPreconditionHeaders { } } -type BlockStreamItemOk = (Vec, Option); +type BlockStreamItemOk = (Bytes, Option); type BlockStreamItem = Result; struct Defragmenter> { @@ -589,7 +590,7 @@ impl> Defragmenter { if self.buffer.is_empty() { let (next_block, next_block_hash) = self.block_stream.next().await.unwrap()?; - self.buffer = next_block; + self.buffer = next_block.to_vec(); // TODO TOO MUCH COPY self.hash = next_block_hash; } else if self.buffer.len() + peeked_next_block.len() > self.block_size { break; @@ -600,7 +601,10 @@ impl> Defragmenter { } } - Ok((std::mem::take(&mut self.buffer), self.hash.take())) + Ok(( + Bytes::from(std::mem::take(&mut self.buffer)), + self.hash.take(), + )) } } diff --git a/src/api/s3/get.rs b/src/api/s3/get.rs index 7fa1a177..7d118f89 100644 --- a/src/api/s3/get.rs +++ b/src/api/s3/get.rs @@ -242,10 +242,13 @@ pub async fn handle_get( Ok(resp_builder.body(body)?) } ObjectVersionData::FirstBlock(_, first_block_hash) => { - let read_first_block = garage.block_manager.rpc_get_block(first_block_hash); + let read_first_block = garage + .block_manager + .rpc_get_block_streaming(first_block_hash); let get_next_blocks = garage.version_table.get(&last_v.uuid, &EmptyKey); - let (first_block, version) = futures::try_join!(read_first_block, get_next_blocks)?; + let (first_block_stream, version) = + futures::try_join!(read_first_block, get_next_blocks)?; let version = version.ok_or(Error::NoSuchKey)?; let mut blocks = version @@ -254,24 +257,32 @@ pub async fn handle_get( .iter() .map(|(_, vb)| (vb.hash, None)) .collect::>(); - blocks[0].1 = Some(first_block); + blocks[0].1 = Some(first_block_stream); let body_stream = futures::stream::iter(blocks) - .map(move |(hash, data_opt)| { + .map(move |(hash, stream_opt)| { let garage = garage.clone(); async move { - if let Some(data) = data_opt { - Ok(Bytes::from(data)) + if let Some(stream) = stream_opt { + stream } else { garage .block_manager - .rpc_get_block(&hash) + .rpc_get_block_streaming(&hash) .await - .map(Bytes::from) + .unwrap_or_else(|_| { + Box::pin(futures::stream::once(async move { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Could not get next block", + )) + })) + }) } } }) - .buffered(2); + .buffered(3) + .flatten(); let body = hyper::body::Body::wrap_stream(body_stream); Ok(resp_builder.body(body)?) diff --git a/src/block/Cargo.toml b/src/block/Cargo.toml index 2555a44a..3e6f7bc0 100644 --- a/src/block/Cargo.toml +++ b/src/block/Cargo.toml @@ -27,6 +27,8 @@ bytes = "1.0" hex = "0.4" tracing = "0.1.30" rand = "0.8" + +async-compression = { version = "0.3", features = ["tokio", "zstd"] } zstd = { version = "0.9", default-features = false } rmp-serde = "0.15" @@ -36,3 +38,4 @@ serde_bytes = "0.11" futures = "0.3" futures-util = "0.3" tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] } +tokio-util = { version = "0.6", features = ["io"] } diff --git a/src/block/block.rs b/src/block/block.rs index f17bd2c0..935aa900 100644 --- a/src/block/block.rs +++ b/src/block/block.rs @@ -5,13 +5,18 @@ use zstd::stream::{decode_all as zstd_decode, Encoder}; use garage_util::data::*; use garage_util::error::*; +#[derive(Debug, Serialize, Deserialize, Copy, Clone)] +pub enum DataBlockHeader { + Plain, + Compressed, +} + /// A possibly compressed block of data -#[derive(Debug, Serialize, Deserialize)] pub enum DataBlock { /// Uncompressed data - Plain(#[serde(with = "serde_bytes")] Vec), + Plain(Bytes), /// Data compressed with zstd - Compressed(#[serde(with = "serde_bytes")] Vec), + Compressed(Bytes), } impl DataBlock { @@ -31,7 +36,7 @@ impl DataBlock { /// Get the buffer, possibly decompressing it, and verify it's integrity. /// For Plain block, data is compared to hash, for Compressed block, zstd checksumming system /// is used instead. - pub fn verify_get(self, hash: Hash) -> Result, Error> { + pub fn verify_get(self, hash: Hash) -> Result { match self { DataBlock::Plain(data) => { if blake2sum(&data) == hash { @@ -40,9 +45,9 @@ impl DataBlock { Err(Error::CorruptData(hash)) } } - DataBlock::Compressed(data) => { - zstd_decode(&data[..]).map_err(|_| Error::CorruptData(hash)) - } + DataBlock::Compressed(data) => zstd_decode(&data[..]) + .map_err(|_| Error::CorruptData(hash)) + .map(Bytes::from), } } @@ -66,14 +71,28 @@ impl DataBlock { tokio::task::spawn_blocking(move || { if let Some(level) = level { if let Ok(data) = zstd_encode(&data[..], level) { - return DataBlock::Compressed(data); + return DataBlock::Compressed(data.into()); } } - DataBlock::Plain(data.to_vec()) // TODO: remove to_vec here + DataBlock::Plain(data) }) .await .unwrap() } + + pub fn into_parts(self) -> (DataBlockHeader, Bytes) { + match self { + DataBlock::Plain(data) => (DataBlockHeader::Plain, data), + DataBlock::Compressed(data) => (DataBlockHeader::Compressed, data), + } + } + + pub fn from_parts(h: DataBlockHeader, bytes: Bytes) -> Self { + match h { + DataBlockHeader::Plain => DataBlock::Plain(bytes), + DataBlockHeader::Compressed => DataBlock::Compressed(bytes), + } + } } fn zstd_encode(mut source: R, level: i32) -> std::io::Result> { diff --git a/src/block/manager.rs b/src/block/manager.rs index 408de148..bb01c300 100644 --- a/src/block/manager.rs +++ b/src/block/manager.rs @@ -1,5 +1,6 @@ use std::convert::TryInto; use std::path::PathBuf; +use std::pin::Pin; use std::sync::Arc; use std::time::Duration; @@ -8,8 +9,10 @@ use async_trait::async_trait; use bytes::Bytes; use serde::{Deserialize, Serialize}; +use futures::{Stream, TryStreamExt}; +use futures_util::stream::StreamExt; use tokio::fs; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader}; use tokio::select; use tokio::sync::{mpsc, watch, Mutex, Notify}; @@ -18,6 +21,8 @@ use opentelemetry::{ Context, KeyValue, }; +use garage_rpc::rpc_helper::netapp::stream::{stream_asyncread, ByteStream}; + use garage_db as db; use garage_db::counted_tree_hack::CountedTree; @@ -70,7 +75,7 @@ pub enum BlockRpc { /// block PutBlock { hash: Hash, - data: DataBlock, + header: DataBlockHeader, }, /// Ask other node if they should have this block, but don't actually have it NeedBlockQuery(Hash), @@ -174,56 +179,146 @@ impl BlockManager { } /// Ask nodes that might have a (possibly compressed) block for it + /// Return it as a stream with a header + async fn rpc_get_raw_block_streaming( + &self, + hash: &Hash, + ) -> Result<(DataBlockHeader, ByteStream), Error> { + let who = self.replication.read_nodes(hash); + + for node in who.iter() { + let node_id = NodeID::from(*node); + let rpc = + self.endpoint + .call_streaming(&node_id, BlockRpc::GetBlock(*hash), PRIO_NORMAL); + tokio::select! { + res = rpc => { + let res = match res { + Ok(res) => res, + Err(e) => { + debug!("Node {:?} returned error: {}", node, e); + continue; + } + }; + let (header, stream) = match res.into_parts() { + (Ok(BlockRpc::PutBlock { hash: _, header }), Some(stream)) => (header, stream), + _ => { + debug!("Node {:?} returned a malformed response", node); + continue; + } + }; + return Ok((header, stream)); + } + _ = tokio::time::sleep(BLOCK_RW_TIMEOUT) => { + debug!("Node {:?} didn't return block in time, trying next.", node); + } + }; + } + + Err(Error::Message(format!( + "Unable to read block {:?}: no node returned a valid block", + hash + ))) + } + + /// Ask nodes that might have a (possibly compressed) block for it + /// Return its entire body async fn rpc_get_raw_block(&self, hash: &Hash) -> Result { let who = self.replication.read_nodes(hash); - let resps = self - .system - .rpc - .try_call_many( - &self.endpoint, - &who[..], - BlockRpc::GetBlock(*hash), - RequestStrategy::with_priority(PRIO_NORMAL) - .with_quorum(1) - .with_timeout(BLOCK_RW_TIMEOUT) - .interrupt_after_quorum(true), - ) - .await?; - for resp in resps { - if let BlockRpc::PutBlock { data, .. } = resp { - return Ok(data); - } + for node in who.iter() { + let node_id = NodeID::from(*node); + let rpc = + self.endpoint + .call_streaming(&node_id, BlockRpc::GetBlock(*hash), PRIO_NORMAL); + tokio::select! { + res = rpc => { + let res = match res { + Ok(res) => res, + Err(e) => { + debug!("Node {:?} returned error: {}", node, e); + continue; + } + }; + let (header, stream) = match res.into_parts() { + (Ok(BlockRpc::PutBlock { hash: _, header }), Some(stream)) => (header, stream), + _ => { + debug!("Node {:?} returned a malformed response", node); + continue; + } + }; + match read_stream_to_end(stream).await { + Ok(bytes) => return Ok(DataBlock::from_parts(header, bytes)), + Err(e) => { + debug!("Error reading stream from node {:?}: {}", node, e); + } + } + } + _ = tokio::time::sleep(BLOCK_RW_TIMEOUT) => { + debug!("Node {:?} didn't return block in time, trying next.", node); + } + }; } + Err(Error::Message(format!( - "Unable to read block {:?}: no valid blocks returned", + "Unable to read block {:?}: no node returned a valid block", hash ))) } // ---- Public interface ---- + /// Ask nodes that might have a block for it, + /// return it as a stream + pub async fn rpc_get_block_streaming( + &self, + hash: &Hash, + ) -> Result< + Pin> + Send + Sync + 'static>>, + Error, + > { + let (header, stream) = self.rpc_get_raw_block_streaming(hash).await?; + match header { + DataBlockHeader::Plain => Ok(Box::pin(stream.map_err(|_| { + std::io::Error::new(std::io::ErrorKind::Other, "netapp stream error") + }))), + DataBlockHeader::Compressed => { + // Too many things, I hate it. + let reader = stream_asyncread(stream); + let reader = BufReader::new(reader); + let reader = async_compression::tokio::bufread::ZstdDecoder::new(reader); + Ok(Box::pin(tokio_util::io::ReaderStream::new(reader))) + } + } + } + /// Ask nodes that might have a block for it - pub async fn rpc_get_block(&self, hash: &Hash) -> Result, Error> { + pub async fn rpc_get_block(&self, hash: &Hash) -> Result { self.rpc_get_raw_block(hash).await?.verify_get(*hash) } /// Send block to nodes that should have it pub async fn rpc_put_block(&self, hash: Hash, data: Bytes) -> Result<(), Error> { let who = self.replication.write_nodes(&hash); - let data = DataBlock::from_buffer(data, self.compression_level).await; + + let (header, bytes) = DataBlock::from_buffer(data, self.compression_level) + .await + .into_parts(); + let put_block_rpc = + Req::new(BlockRpc::PutBlock { hash, header })?.with_stream_from_buffer(bytes); + self.system .rpc .try_call_many( &self.endpoint, &who[..], - // TODO: remove to_vec() here - BlockRpc::PutBlock { hash, data }, + put_block_rpc, RequestStrategy::with_priority(PRIO_NORMAL) .with_quorum(self.replication.write_quorum()) .with_timeout(BLOCK_RW_TIMEOUT), ) .await?; + Ok(()) } @@ -308,13 +403,25 @@ impl BlockManager { // ---- Reading and writing blocks locally ---- + async fn handle_put_block( + &self, + hash: Hash, + header: DataBlockHeader, + stream: Option, + ) -> Result<(), Error> { + let stream = stream.ok_or_message("missing stream")?; + let bytes = read_stream_to_end(stream).await?; + let data = DataBlock::from_parts(header, bytes); + self.write_block(&hash, &data).await + } + /// Write a block to disk - async fn write_block(&self, hash: &Hash, data: &DataBlock) -> Result { + async fn write_block(&self, hash: &Hash, data: &DataBlock) -> Result<(), Error> { let tracer = opentelemetry::global::tracer("garage"); let write_size = data.inner_buffer().len() as u64; - let res = self.mutation_lock[hash.as_slice()[0] as usize] + self.mutation_lock[hash.as_slice()[0] as usize] .lock() .with_context(Context::current_with_span( tracer.start("Acquire mutation_lock"), @@ -329,21 +436,31 @@ impl BlockManager { self.metrics.bytes_written.add(write_size); - Ok(res) + Ok(()) + } + + async fn handle_get_block(&self, hash: &Hash) -> Resp { + let block = match self.read_block(hash).await { + Ok(data) => data, + Err(e) => return Resp::new(Err(e)), + }; + + let (header, data) = block.into_parts(); + + self.metrics.bytes_read.add(data.len() as u64); + + Resp::new(Ok(BlockRpc::PutBlock { + hash: *hash, + header, + })) + .with_stream_from_buffer(data) } /// Read block from disk, verifying it's integrity - pub(crate) async fn read_block(&self, hash: &Hash) -> Result { - let data = self - .read_block_internal(hash) + pub(crate) async fn read_block(&self, hash: &Hash) -> Result { + self.read_block_internal(hash) .bound_record_duration(&self.metrics.block_read_duration) - .await?; - - self.metrics - .bytes_read - .add(data.inner_buffer().len() as u64); - - Ok(BlockRpc::PutBlock { hash: *hash, data }) + .await } async fn read_block_internal(&self, hash: &Hash) -> Result { @@ -366,9 +483,9 @@ impl BlockManager { drop(f); let data = if compressed { - DataBlock::Compressed(data) + DataBlock::Compressed(data.into()) } else { - DataBlock::Plain(data) + DataBlock::Plain(data.into()) }; if data.verify(*hash).is_err() { @@ -675,7 +792,13 @@ impl BlockManager { .add(1, &[KeyValue::new("to", format!("{:?}", node))]); } - let put_block_message = self.read_block(hash).await?; + let block = self.read_block(hash).await?; + let (header, bytes) = block.into_parts(); + let put_block_message = Req::new(BlockRpc::PutBlock { + hash: *hash, + header, + })? + .with_stream_from_buffer(bytes); self.system .rpc .try_call_many( @@ -723,17 +846,19 @@ impl BlockManager { } #[async_trait] -impl EndpointHandler for BlockManager { - async fn handle( - self: &Arc, - message: &BlockRpc, - _from: NodeID, - ) -> Result { - match message { - BlockRpc::PutBlock { hash, data } => self.write_block(hash, data).await, - BlockRpc::GetBlock(h) => self.read_block(h).await, - BlockRpc::NeedBlockQuery(h) => self.need_block(h).await.map(BlockRpc::NeedBlockReply), - m => Err(Error::unexpected_rpc_message(m)), +impl StreamingEndpointHandler for BlockManager { + async fn handle(self: &Arc, mut message: Req, _from: NodeID) -> Resp { + match message.msg() { + BlockRpc::PutBlock { hash, header } => Resp::new( + self.handle_put_block(*hash, *header, message.take_stream()) + .await + .map(|_| BlockRpc::Ok), + ), + BlockRpc::GetBlock(h) => self.handle_get_block(h).await, + BlockRpc::NeedBlockQuery(h) => { + Resp::new(self.need_block(h).await.map(BlockRpc::NeedBlockReply)) + } + m => Resp::new(Err(Error::unexpected_rpc_message(m))), } } } @@ -831,7 +956,7 @@ impl BlockManagerLocked { hash: &Hash, data: &DataBlock, mgr: &BlockManager, - ) -> Result { + ) -> Result<(), Error> { let compressed = data.is_compressed(); let data = data.inner_buffer(); @@ -842,8 +967,8 @@ impl BlockManagerLocked { fs::create_dir_all(&directory).await?; let to_delete = match (mgr.is_block_compressed(hash).await, compressed) { - (Ok(true), _) => return Ok(BlockRpc::Ok), - (Ok(false), false) => return Ok(BlockRpc::Ok), + (Ok(true), _) => return Ok(()), + (Ok(false), false) => return Ok(()), (Ok(false), true) => { let path_to_delete = path.clone(); path.set_extension("zst"); @@ -882,7 +1007,7 @@ impl BlockManagerLocked { dir.sync_all().await?; drop(dir); - Ok(BlockRpc::Ok) + Ok(()) } async fn move_block_to_corrupted(&self, hash: &Hash, mgr: &BlockManager) -> Result<(), Error> { @@ -963,3 +1088,17 @@ impl ErrorCounter { self.last_try + self.delay_msec() } } + +async fn read_stream_to_end(mut stream: ByteStream) -> Result { + let mut parts: Vec = vec![]; + while let Some(part) = stream.next().await { + parts.push(part.ok_or_message("error in stream")?); + } + + Ok(parts + .iter() + .map(|x| &x[..]) + .collect::>() + .concat() + .into()) +} diff --git a/src/rpc/rpc_helper.rs b/src/rpc/rpc_helper.rs index 079cdc70..6e098446 100644 --- a/src/rpc/rpc_helper.rs +++ b/src/rpc/rpc_helper.rs @@ -15,10 +15,13 @@ use opentelemetry::{ Context, }; -pub use netapp::endpoint::{Endpoint, EndpointHandler}; -pub use netapp::message::{Message as Rpc, *}; +pub use netapp::endpoint::{Endpoint, EndpointHandler, StreamingEndpointHandler}; +use netapp::message::IntoReq; +pub use netapp::message::{ + Message as Rpc, Req, RequestPriority, Resp, PRIO_BACKGROUND, PRIO_HIGH, PRIO_NORMAL, +}; use netapp::peering::fullmesh::FullMeshPeeringStrategy; -pub use netapp::{NetApp, NodeID}; +pub use netapp::{self, NetApp, NodeID}; use garage_util::background::BackgroundRunner; use garage_util::data::*; @@ -117,7 +120,7 @@ impl RpcHelper { where M: Rpc>, N: IntoReq + Send, - H: EndpointHandler, + H: StreamingEndpointHandler, { let metric_tags = [ KeyValue::new("rpc_endpoint", endpoint.path().to_string()), @@ -172,7 +175,7 @@ impl RpcHelper { where M: Rpc>, N: IntoReq, - H: EndpointHandler, + H: StreamingEndpointHandler, { let msg = msg.into_req().map_err(netapp::error::Error::from)?; @@ -197,7 +200,7 @@ impl RpcHelper { where M: Rpc>, N: IntoReq, - H: EndpointHandler, + H: StreamingEndpointHandler, { let to = self .0 @@ -211,16 +214,17 @@ impl RpcHelper { /// Make a RPC call to multiple servers, returning either a Vec of responses, /// or an error if quorum could not be reached due to too many errors - pub async fn try_call_many( + pub async fn try_call_many( &self, endpoint: &Arc>, to: &[Uuid], - msg: M, + msg: N, strategy: RequestStrategy, ) -> Result, Error> where M: Rpc> + 'static, - H: EndpointHandler + 'static, + N: IntoReq, + H: StreamingEndpointHandler + 'static, S: Send + 'static, { let quorum = strategy.rs_quorum.unwrap_or(to.len()); @@ -261,7 +265,7 @@ impl RpcHelper { where M: Rpc> + 'static, N: IntoReq, - H: EndpointHandler + 'static, + H: StreamingEndpointHandler + 'static, S: Send + 'static, { let msg = msg.into_req().map_err(netapp::error::Error::from)?; From 68087ee13dc22dbaeb0c1fa8dcb4bdbaa82098a6 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 22 Jul 2022 19:06:56 +0200 Subject: [PATCH 053/149] Fix clippy --- src/api/s3/copy.rs | 5 +---- src/api/s3/get.rs | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/api/s3/copy.rs b/src/api/s3/copy.rs index 54a565e0..b54cbd23 100644 --- a/src/api/s3/copy.rs +++ b/src/api/s3/copy.rs @@ -366,10 +366,7 @@ pub async fn handle_upload_part_copy( // we need to insert that data as a new block. async move { if must_upload { - garage2 - .block_manager - .rpc_put_block(final_hash, data.into()) - .await + garage2.block_manager.rpc_put_block(final_hash, data).await } else { Ok(()) } diff --git a/src/api/s3/get.rs b/src/api/s3/get.rs index 7d118f89..c7621ade 100644 --- a/src/api/s3/get.rs +++ b/src/api/s3/get.rs @@ -450,7 +450,6 @@ fn body_from_blocks_range( let garage = garage.clone(); async move { let data = garage.block_manager.rpc_get_block(&block.hash).await?; - let data = Bytes::from(data); let start_in_block = if true_offset > begin { 0 } else { From 33750c04ed680732a2aa6a695dd5f4fea6d9a393 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 22 Jul 2022 19:10:23 +0200 Subject: [PATCH 054/149] Update cargo.nix --- Cargo.nix | 176 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 103 insertions(+), 73 deletions(-) diff --git a/Cargo.nix b/Cargo.nix index 144967ee..ec828ae9 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -125,6 +125,28 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".async-compression."0.3.10" = overridableMkRustCrate (profileName: rec { + name = "async-compression"; + version = "0.3.10"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "00461f243d703f6999c8e7494f077799f1362720a55ae49a90ffe6214032fc0b"; }; + features = builtins.concatLists [ + [ "default" ] + [ "libzstd" ] + [ "tokio" ] + [ "zstd" ] + [ "zstd-safe" ] + ]; + dependencies = { + futures_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; + memchr = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; }; + pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; + tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; + libzstd = rustPackages."registry+https://github.com/rust-lang/crates.io-index".zstd."0.9.2+zstd.1.5.1" { inherit profileName; }; + zstd_safe = rustPackages."registry+https://github.com/rust-lang/crates.io-index".zstd-safe."4.1.3+zstd.1.5.1" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".async-stream."0.3.3" = overridableMkRustCrate (profileName: rec { name = "async-stream"; version = "0.3.3"; @@ -242,7 +264,7 @@ in aws_smithy_types = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-types."0.38.0" { inherit profileName; }; aws_smithy_xml = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-xml."0.38.0" { inherit profileName; }; aws_types = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-types."0.8.0" { inherit profileName; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; md5 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".md5."0.7.0" { inherit profileName; }; tokio_stream = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.8" { inherit profileName; }; @@ -288,7 +310,7 @@ in dependencies = { aws_smithy_eventstream = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-eventstream."0.38.0" { inherit profileName; }; aws_smithy_http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-http."0.38.0" { inherit profileName; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; form_urlencoded = rustPackages."registry+https://github.com/rust-lang/crates.io-index".form_urlencoded."1.0.1" { inherit profileName; }; hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; @@ -335,7 +357,7 @@ in aws_smithy_http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-http."0.38.0" { inherit profileName; }; aws_smithy_http_tower = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-http-tower."0.38.0" { inherit profileName; }; aws_smithy_types = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-types."0.38.0" { inherit profileName; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; fastrand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".fastrand."1.7.0" { inherit profileName; }; http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; http_body = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."0.4.4" { inherit profileName; }; @@ -357,7 +379,7 @@ in src = fetchCratesIo { inherit name version; sha256 = "f972226c639e0dc1eca2cb0220c1b5799e2bfc62eda37845b662c5d0cb972371"; }; dependencies = { aws_smithy_types = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-types."0.38.0" { inherit profileName; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; crc32fast = rustPackages."registry+https://github.com/rust-lang/crates.io-index".crc32fast."1.3.2" { inherit profileName; }; }; }); @@ -377,7 +399,7 @@ in dependencies = { aws_smithy_eventstream = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-eventstream."0.38.0" { inherit profileName; }; aws_smithy_types = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-types."0.38.0" { inherit profileName; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; bytes_utils = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes-utils."0.1.2" { inherit profileName; }; futures_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; @@ -398,7 +420,7 @@ in src = fetchCratesIo { inherit name version; sha256 = "64f80a2c56fc09fc9a2da3c63f286ec2a89465433219f8165e14e522283a5eb8"; }; dependencies = { aws_smithy_http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-http."0.38.0" { inherit profileName; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; http_body = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."0.4.4" { inherit profileName; }; pin_project = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; }; @@ -558,11 +580,11 @@ in ]; }); - "registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" = overridableMkRustCrate (profileName: rec { + "registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" = overridableMkRustCrate (profileName: rec { name = "bytes"; - version = "1.1.0"; + version = "1.2.0"; registry = "registry+https://github.com/rust-lang/crates.io-index"; - src = fetchCratesIo { inherit name version; sha256 = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"; }; + src = fetchCratesIo { inherit name version; sha256 = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e"; }; features = builtins.concatLists [ [ "default" ] [ "std" ] @@ -579,7 +601,7 @@ in [ "std" ] ]; dependencies = { - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; either = rustPackages."registry+https://github.com/rust-lang/crates.io-index".either."1.6.1" { inherit profileName; }; }; }); @@ -759,7 +781,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -1403,7 +1425,7 @@ in ]; dependencies = { async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; bytesize = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytesize."1.1.0" { inherit profileName; }; futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; @@ -1417,7 +1439,7 @@ in garage_web = rustPackages."unknown".garage_web."0.7.0" { inherit profileName; }; hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; sodiumoxide = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; - netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; + netapp = rustPackages."git+https://git.deuxfleurs.fr/lx/netapp".netapp."0.5.0" { inherit profileName; }; opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; opentelemetry_otlp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-otlp."0.10.0" { inherit profileName; }; opentelemetry_prometheus = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-prometheus."0.10.0" { inherit profileName; }; @@ -1458,7 +1480,7 @@ in dependencies = { ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "async_trait" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "base64" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.13.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "chrono" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.19" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "crypto_common" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".crypto-common."0.1.6" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "err_derive" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; @@ -1505,8 +1527,9 @@ in src = fetchCrateLocal (workspaceSrc + "/src/block"); dependencies = { arc_swap = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; + async_compression = rustPackages."registry+https://github.com/rust-lang/crates.io-index".async-compression."0.3.10" { inherit profileName; }; async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; garage_db = rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }; @@ -1520,6 +1543,7 @@ in serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; serde_bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; + tokio_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.6.9" { inherit profileName; }; tracing = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; zstd = rustPackages."registry+https://github.com/rust-lang/crates.io-index".zstd."0.9.2+zstd.1.5.1" { inherit profileName; }; }; @@ -1599,7 +1623,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_table" else null } = rustPackages."unknown".garage_table."0.7.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."git+https://git.deuxfleurs.fr/lx/netapp".netapp."0.5.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; @@ -1619,7 +1643,7 @@ in dependencies = { arc_swap = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; garage_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".garage_util."0.5.1" { inherit profileName; }; @@ -1654,7 +1678,7 @@ in dependencies = { ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "arc_swap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "async_trait" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; @@ -1665,7 +1689,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "k8s_openapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".k8s-openapi."0.13.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "kube" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kube."0.62.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "sodiumoxide" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."git+https://git.deuxfleurs.fr/lx/netapp".netapp."0.5.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "openssl" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".openssl."0.10.38" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "pnet_datalink" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pnet_datalink."0.28.0" { inherit profileName; }; @@ -1688,7 +1712,7 @@ in src = fetchCratesIo { inherit name version; sha256 = "5c3557f3757e2acd29eaee86804d4e6c38d2abda81b4b349d8a0d2277044265c"; }; dependencies = { async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; garage_rpc = rustPackages."registry+https://github.com/rust-lang/crates.io-index".garage_rpc."0.5.1" { inherit profileName; }; @@ -1711,7 +1735,7 @@ in src = fetchCrateLocal (workspaceSrc + "/src/table"); dependencies = { async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; garage_db = rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }; @@ -1766,7 +1790,7 @@ in dependencies = { ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "async_trait" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "blake2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".blake2."0.9.2" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "chrono" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.19" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "digest" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".digest."0.10.3" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "err_derive" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; @@ -1775,7 +1799,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "http" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "hyper" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "netapp" else null } = rustPackages."git+https://git.deuxfleurs.fr/lx/netapp".netapp."0.5.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; @@ -1881,7 +1905,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "62eeb471aa3e3c9197aa4bfeabfe02982f6dc96f750486c0bb0009ac58b26d2b"; }; dependencies = { - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; fnv = rustPackages."registry+https://github.com/rust-lang/crates.io-index".fnv."1.0.7" { inherit profileName; }; futures_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; futures_sink = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-sink."0.3.21" { inherit profileName; }; @@ -2062,7 +2086,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03"; }; dependencies = { - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; fnv = rustPackages."registry+https://github.com/rust-lang/crates.io-index".fnv."1.0.7" { inherit profileName; }; itoa = rustPackages."registry+https://github.com/rust-lang/crates.io-index".itoa."1.0.1" { inherit profileName; }; }; @@ -2074,7 +2098,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6"; }; dependencies = { - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; }; @@ -2141,7 +2165,7 @@ in (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "tcp") ]; dependencies = { - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_channel" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-channel."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; @@ -2204,7 +2228,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"; }; dependencies = { - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; hyper = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; native_tls = rustPackages."registry+https://github.com/rust-lang/crates.io-index".native-tls."0.2.8" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; @@ -2406,7 +2430,7 @@ in ]; dependencies = { base64 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.13.0" { inherit profileName; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; chrono = rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.19" { inherit profileName; }; http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; percent_encoding = rustPackages."registry+https://github.com/rust-lang/crates.io-index".percent-encoding."2.1.0" { inherit profileName; }; @@ -2477,7 +2501,7 @@ in ]; dependencies = { base64 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.13.0" { inherit profileName; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; chrono = rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.19" { inherit profileName; }; dirs = rustPackages."registry+https://github.com/rust-lang/crates.io-index".dirs-next."2.0.0" { inherit profileName; }; either = rustPackages."registry+https://github.com/rust-lang/crates.io-index".either."1.6.1" { inherit profileName; }; @@ -2846,7 +2870,7 @@ in [ "default" ] ]; dependencies = { - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; encoding_rs = rustPackages."registry+https://github.com/rust-lang/crates.io-index".encoding_rs."0.8.30" { inherit profileName; }; futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; @@ -2913,37 +2937,43 @@ in }; }); - "registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" = overridableMkRustCrate (profileName: rec { + "git+https://git.deuxfleurs.fr/lx/netapp".netapp."0.5.0" = overridableMkRustCrate (profileName: rec { name = "netapp"; - version = "0.4.4"; - registry = "registry+https://github.com/rust-lang/crates.io-index"; - src = fetchCratesIo { inherit name version; sha256 = "c6419a4b836774192e13fedb05c0e5f414ee8df9ca0c467456a0bacde06c29ee"; }; + version = "0.5.0"; + registry = "git+https://git.deuxfleurs.fr/lx/netapp"; + src = fetchCrateGit { + url = https://git.deuxfleurs.fr/lx/netapp; + name = "netapp"; + version = "0.5.0"; + rev = "ab80ade4f0034cbdcf15a99c674730f85eb06870"; + ref = "stream-body";}; features = builtins.concatLists [ - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "opentelemetry") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "opentelemetry-contrib") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "rand") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "telemetry") + [ "default" ] + [ "opentelemetry" ] + [ "opentelemetry-contrib" ] + [ "rand" ] + [ "telemetry" ] ]; dependencies = { - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "arc_swap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "async_trait" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."0.6.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "cfg_if" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "err_derive" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.2.4" { profileName = "__noProfile"; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "kuska_handshake" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-handshake."0.2.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "sodiumoxide" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "log" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "opentelemetry_contrib" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-contrib."0.9.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.5.6" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.14.4" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio_stream" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.8" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.6.9" { inherit profileName; }; + arc_swap = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; + async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; + cfg_if = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }; + err_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.2.4" { profileName = "__noProfile"; }; + futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; + hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; + kuska_handshake = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-handshake."0.2.0" { inherit profileName; }; + sodiumoxide = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; + log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; + opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; + opentelemetry_contrib = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-contrib."0.9.0" { inherit profileName; }; + pin_project = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; }; + rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.5.6" { inherit profileName; }; + rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.14.4" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; + tokio_stream = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.8" { inherit profileName; }; + tokio_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.6.9" { inherit profileName; }; }; }); @@ -3572,7 +3602,7 @@ in [ "std" ] ]; dependencies = { - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; prost_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".prost-derive."0.9.0" { profileName = "__noProfile"; }; }; }); @@ -3583,7 +3613,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5"; }; dependencies = { - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; heck = rustPackages."registry+https://github.com/rust-lang/crates.io-index".heck."0.3.3" { inherit profileName; }; itertools = rustPackages."registry+https://github.com/rust-lang/crates.io-index".itertools."0.10.3" { inherit profileName; }; lazy_static = rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { inherit profileName; }; @@ -3620,7 +3650,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a"; }; dependencies = { - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; prost = rustPackages."registry+https://github.com/rust-lang/crates.io-index".prost."0.9.0" { inherit profileName; }; }; }); @@ -3990,7 +4020,7 @@ in ]; dependencies = { ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" || hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "i686" || hostPlatform.parsed.cpu.name == "x86_64" || (hostPlatform.parsed.cpu.name == "aarch64" || hostPlatform.parsed.cpu.name == "armv6l" || hostPlatform.parsed.cpu.name == "armv7l") && (hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "fuchsia" || hostPlatform.parsed.kernel.name == "linux") then "spin" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".spin."0.5.2" { inherit profileName; }; untrusted = rustPackages."registry+https://github.com/rust-lang/crates.io-index".untrusted."0.7.1" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "wasm32" && hostPlatform.parsed.vendor.name == "unknown" && hostPlatform.parsed.kernel.name == "unknown" && hostPlatform.parsed.abi.name == "" then "web_sys" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.56" { inherit profileName; }; @@ -4063,7 +4093,7 @@ in dependencies = { async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; base64 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.13.0" { inherit profileName; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; crc32fast = rustPackages."registry+https://github.com/rust-lang/crates.io-index".crc32fast."1.3.2" { inherit profileName; }; futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; @@ -4109,7 +4139,7 @@ in src = fetchCratesIo { inherit name version; sha256 = "a5ae95491c8b4847931e291b151127eccd6ff8ca13f33603eb3d0035ecb05272"; }; dependencies = { base64 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.13.0" { inherit profileName; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; chrono = rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.19" { inherit profileName; }; digest = rustPackages."registry+https://github.com/rust-lang/crates.io-index".digest."0.9.0" { inherit profileName; }; futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; @@ -4885,7 +4915,7 @@ in (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "winapi") ]; dependencies = { - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; ${ if (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") && hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "memchr" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "mio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".mio."0.8.2" { inherit profileName; }; @@ -4971,14 +5001,14 @@ in features = builtins.concatLists [ (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "codec") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "compat") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "futures-io") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "io") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "io") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "slab") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "time") ]; dependencies = { - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_io" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-io."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_sink" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-sink."0.3.21" { inherit profileName; }; @@ -4995,7 +5025,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "64910e1b9c1901aaf5375561e35b9c057d95ff41a44ede043a03e09279eabaf1"; }; dependencies = { - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; futures_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; futures_sink = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-sink."0.3.21" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; @@ -5041,7 +5071,7 @@ in async_stream = rustPackages."registry+https://github.com/rust-lang/crates.io-index".async-stream."0.3.3" { inherit profileName; }; async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; base64 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.13.0" { inherit profileName; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; futures_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; h2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".h2."0.3.12" { inherit profileName; }; @@ -5143,7 +5173,7 @@ in ]; dependencies = { base64 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.13.0" { inherit profileName; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; futures_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; @@ -5622,11 +5652,11 @@ in [ "default" ] ]; dependencies = { - ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-uwp-windows-gnu" || hostPlatform.config == "i686-pc-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); From 126b0373074600695a46ac24933b2330ee076ea9 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 25 Jul 2022 11:02:55 +0200 Subject: [PATCH 055/149] update netapp --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 9d12f523..0d00bca1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2158,7 +2158,7 @@ dependencies = [ [[package]] name = "netapp" version = "0.5.0" -source = "git+https://git.deuxfleurs.fr/lx/netapp?branch=stream-body#ab80ade4f0034cbdcf15a99c674730f85eb06870" +source = "git+https://git.deuxfleurs.fr/lx/netapp?branch=stream-body#fed0542313824df295a7e322a9aebe8ba62f97b9" dependencies = [ "arc-swap", "async-trait", From f0ee3056d3357221d71ad7d346e419dc26b9e063 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 25 Jul 2022 11:13:12 +0200 Subject: [PATCH 056/149] Update cargo.nix --- Cargo.nix | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.nix b/Cargo.nix index ec828ae9..a6c42b40 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -781,7 +781,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.config == "aarch64-linux-android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -2832,7 +2832,7 @@ in [ "os-poll" ] ]; dependencies = { - ${ if hostPlatform.parsed.kernel.name == "wasi" || hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "wasi" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if hostPlatform.isWindows then "miow" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".miow."0.3.7" { inherit profileName; }; ${ if hostPlatform.isWindows then "ntapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ntapi."0.3.7" { inherit profileName; }; @@ -2945,7 +2945,7 @@ in url = https://git.deuxfleurs.fr/lx/netapp; name = "netapp"; version = "0.5.0"; - rev = "ab80ade4f0034cbdcf15a99c674730f85eb06870"; + rev = "fed0542313824df295a7e322a9aebe8ba62f97b9"; ref = "stream-body";}; features = builtins.concatLists [ [ "default" ] @@ -4632,7 +4632,7 @@ in ]; dependencies = { bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot_core."0.8.5" { inherit profileName; }; static_init_macro = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init_macro."1.0.2" { profileName = "__noProfile"; }; @@ -5652,10 +5652,10 @@ in [ "default" ] ]; dependencies = { - ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-uwp-windows-gnu" || hostPlatform.config == "i686-pc-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); From e935861854deed5d1ca66767fc51d9849201a4dd Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 25 Jul 2022 18:19:35 +0200 Subject: [PATCH 057/149] Factor out node request order selection logic & use in manager --- Cargo.lock | 2 +- script/dev-cluster.sh | 2 +- src/block/manager.rs | 2 + src/rpc/rpc_helper.rs | 95 +++++++++++++++++++++++++------------------ 4 files changed, 60 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d00bca1..e3d8373f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2158,7 +2158,7 @@ dependencies = [ [[package]] name = "netapp" version = "0.5.0" -source = "git+https://git.deuxfleurs.fr/lx/netapp?branch=stream-body#fed0542313824df295a7e322a9aebe8ba62f97b9" +source = "git+https://git.deuxfleurs.fr/lx/netapp?branch=stream-body#74e57016f63b6052cf6d539812859c3a46138eee" dependencies = [ "arc-swap", "async-trait", diff --git a/script/dev-cluster.sh b/script/dev-cluster.sh index fa0a950e..c7fbe08d 100755 --- a/script/dev-cluster.sh +++ b/script/dev-cluster.sh @@ -11,7 +11,7 @@ PATH="${GARAGE_DEBUG}:${GARAGE_RELEASE}:${NIX_RELEASE}:$PATH" FANCYCOLORS=("41m" "42m" "44m" "45m" "100m" "104m") export RUST_BACKTRACE=1 -export RUST_LOG=garage=info,garage_api=debug +export RUST_LOG=garage=info,garage_api=debug,netapp=trace MAIN_LABEL="\e[${FANCYCOLORS[0]}[main]\e[49m" WHICH_GARAGE=$(which garage || exit 1) diff --git a/src/block/manager.rs b/src/block/manager.rs index bb01c300..80c52510 100644 --- a/src/block/manager.rs +++ b/src/block/manager.rs @@ -185,6 +185,7 @@ impl BlockManager { hash: &Hash, ) -> Result<(DataBlockHeader, ByteStream), Error> { let who = self.replication.read_nodes(hash); + //let who = self.system.rpc.request_order(&who); for node in who.iter() { let node_id = NodeID::from(*node); @@ -225,6 +226,7 @@ impl BlockManager { /// Return its entire body async fn rpc_get_raw_block(&self, hash: &Hash) -> Result { let who = self.replication.read_nodes(hash); + //let who = self.system.rpc.request_order(&who); for node in who.iter() { let node_id = NodeID::from(*node); diff --git a/src/rpc/rpc_helper.rs b/src/rpc/rpc_helper.rs index 6e098446..ddabd636 100644 --- a/src/rpc/rpc_helper.rs +++ b/src/rpc/rpc_helper.rs @@ -292,47 +292,19 @@ impl RpcHelper { // to reach a quorum, priorizing nodes with the lowest latency. // When there are errors, we start new requests to compensate. - // Retrieve some status variables that we will use to sort requests - let peer_list = self.0.fullmesh.get_peer_list(); - let ring: Arc = self.0.ring.borrow().clone(); - let our_zone = match ring.layout.node_role(&self.0.our_node_id) { - Some(pc) => &pc.zone, - None => "", - }; - - // Augment requests with some information used to sort them. - // The tuples are as follows: - // (is another node?, is another zone?, latency, node ID, request future) - // We store all of these tuples in a vec that we can sort. - // By sorting this vec, we priorize ourself, then nodes in the same zone, - // and within a same zone we priorize nodes with the lowest latency. - let mut requests = requests - .map(|(to, fut)| { - let peer_zone = match ring.layout.node_role(&to) { - Some(pc) => &pc.zone, - None => "", - }; - let peer_avg_ping = peer_list - .iter() - .find(|x| x.id.as_ref() == to.as_slice()) - .and_then(|pi| pi.avg_ping) - .unwrap_or_else(|| Duration::from_secs(1)); - ( - to != self.0.our_node_id, - peer_zone != our_zone, - peer_avg_ping, - to, - fut, - ) - }) + // Reorder requests to priorize closeness / low latency + let request_order = self.request_order(to); + let mut ord_requests = vec![(); request_order.len()] + .into_iter() + .map(|_| None) .collect::>(); - - // Sort requests by (priorize ourself, priorize same zone, priorize low latency) - requests - .sort_by_key(|(diffnode, diffzone, ping, _to, _fut)| (*diffnode, *diffzone, *ping)); + for (to, fut) in requests { + let i = request_order.iter().position(|x| *x == to).unwrap(); + ord_requests[i] = Some((to, fut)); + } // Make an iterator to take requests in their sorted order - let mut requests = requests.into_iter(); + let mut requests = ord_requests.into_iter().map(Option::unwrap); // resp_stream will contain all of the requests that are currently in flight. // (for the moment none, they will be added in the loop below) @@ -343,7 +315,7 @@ impl RpcHelper { // If the current set of requests that are running is not enough to possibly // reach quorum, start some new requests. while successes.len() + resp_stream.len() < quorum { - if let Some((_, _, _, req_to, fut)) = requests.next() { + if let Some((req_to, fut)) = requests.next() { let tracer = opentelemetry::global::tracer("garage"); let span = tracer.start(format!("RPC to {:?}", req_to)); resp_stream.push(tokio::spawn( @@ -413,4 +385,49 @@ impl RpcHelper { Err(Error::Quorum(quorum, successes.len(), to.len(), errors)) } } + + pub fn request_order(&self, nodes: &[Uuid]) -> Vec { + // Retrieve some status variables that we will use to sort requests + let peer_list = self.0.fullmesh.get_peer_list(); + let ring: Arc = self.0.ring.borrow().clone(); + let our_zone = match ring.layout.node_role(&self.0.our_node_id) { + Some(pc) => &pc.zone, + None => "", + }; + + // Augment requests with some information used to sort them. + // The tuples are as follows: + // (is another node?, is another zone?, latency, node ID, request future) + // We store all of these tuples in a vec that we can sort. + // By sorting this vec, we priorize ourself, then nodes in the same zone, + // and within a same zone we priorize nodes with the lowest latency. + let mut nodes = nodes + .iter() + .map(|to| { + let peer_zone = match ring.layout.node_role(&to) { + Some(pc) => &pc.zone, + None => "", + }; + let peer_avg_ping = peer_list + .iter() + .find(|x| x.id.as_ref() == to.as_slice()) + .and_then(|pi| pi.avg_ping) + .unwrap_or_else(|| Duration::from_secs(1)); + ( + *to != self.0.our_node_id, + peer_zone != our_zone, + peer_avg_ping, + *to, + ) + }) + .collect::>(); + + // Sort requests by (priorize ourself, priorize same zone, priorize low latency) + nodes.sort_by_key(|(diffnode, diffzone, ping, _to)| (*diffnode, *diffzone, *ping)); + + nodes + .into_iter() + .map(|(_, _, _, to)| to) + .collect::>() + } } From 8cd02639dc688dcb736b5c36dae822706862fac1 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 3 Aug 2022 11:05:15 +0200 Subject: [PATCH 058/149] drone: set TARGET env as needed by "to_s3" func --- .drone.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 0c8a9221..e0a062d7 100644 --- a/.drone.yml +++ b/.drone.yml @@ -72,6 +72,7 @@ steps: from_secret: garagehq_aws_access_key_id AWS_SECRET_ACCESS_KEY: from_secret: garagehq_aws_secret_access_key + TARGET: "x86_64-unknown-linux-musl" commands: - nix-shell --attr release --run "to_s3" @@ -122,6 +123,7 @@ steps: from_secret: garagehq_aws_access_key_id AWS_SECRET_ACCESS_KEY: from_secret: garagehq_aws_secret_access_key + TARGET: "i686-unknown-linux-musl" commands: - nix-shell --attr release --run "to_s3" @@ -166,6 +168,7 @@ steps: from_secret: garagehq_aws_access_key_id AWS_SECRET_ACCESS_KEY: from_secret: garagehq_aws_secret_access_key + TARGET: "aarch64-unknown-linux-musl" commands: - nix-shell --attr release --run "to_s3" @@ -210,6 +213,7 @@ steps: from_secret: garagehq_aws_access_key_id AWS_SECRET_ACCESS_KEY: from_secret: garagehq_aws_secret_access_key + TARGET: "armv6l-unknown-linux-musleabihf" commands: - nix-shell --attr release --run "to_s3" @@ -265,6 +269,6 @@ trigger: --- kind: signature -hmac: 8495114848396ebb492831fc9bd37b353e1a4add9d72c0a123d109490a5b0db0 +hmac: ccec0e06bf6676a705d6a4e63dc322691148bc72e72a4aaa54be6c5fd22652a2 ... From 2c7bae935ac68acab831fe86e5330d3c9a84a953 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 10 Aug 2022 12:18:44 +0200 Subject: [PATCH 059/149] Configure structopt to report the right version By default, structopt reports the value provided by the env var CARGO_PKG_VERSION, feeded by Cargo when reading Cargo.toml. However for Garage we use a versioning based on git, so we often report a version that is behind the real version. In this commit, we create garage_util::version::garage() that reports the right version and configure all structopt subcommands to call this function instead of using the env var. --- .drone.yml | 14 +++--- Cargo.lock | 2 +- Cargo.nix | 20 ++++---- default.nix | 8 +-- nix/compile.nix | 21 ++++---- src/garage/cli/structs.rs | 101 +++++++++++++++++++------------------- src/garage/main.rs | 3 +- src/rpc/Cargo.toml | 1 - src/rpc/system.rs | 7 +-- src/util/Cargo.toml | 2 + src/util/lib.rs | 1 + src/util/version.rs | 7 +++ 12 files changed, 97 insertions(+), 90 deletions(-) create mode 100644 src/util/version.rs diff --git a/.drone.yml b/.drone.yml index e0a062d7..ce576278 100644 --- a/.drone.yml +++ b/.drone.yml @@ -14,7 +14,7 @@ steps: - name: build image: nixpkgs/nix:nixos-22.05 commands: - - nix-build --no-build-output --attr clippy.amd64 --argstr git_version $DRONE_COMMIT + - nix-build --no-build-output --attr clippy.amd64 --argstr git_version ${DRONE_TAG:-$DRONE_COMMIT} - name: unit + func tests image: nixpkgs/nix:nixos-22.05 @@ -34,7 +34,7 @@ steps: - name: integration tests image: nixpkgs/nix:nixos-22.05 commands: - - nix-build --no-build-output --attr clippy.amd64 --argstr git_version $DRONE_COMMIT + - nix-build --no-build-output --attr clippy.amd64 --argstr git_version ${DRONE_TAG:-$DRONE_COMMIT} - nix-shell --attr integration --run ./script/test-smoke.sh || (cat /tmp/garage.log; false) trigger: @@ -57,7 +57,7 @@ steps: - name: build image: nixpkgs/nix:nixos-22.05 commands: - - nix-build --no-build-output --attr pkgs.amd64.release --argstr git_version $DRONE_COMMIT + - nix-build --no-build-output --attr pkgs.amd64.release --argstr git_version ${DRONE_TAG:-$DRONE_COMMIT} - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: integration @@ -108,7 +108,7 @@ steps: - name: build image: nixpkgs/nix:nixos-22.05 commands: - - nix-build --no-build-output --attr pkgs.i386.release --argstr git_version $DRONE_COMMIT + - nix-build --no-build-output --attr pkgs.i386.release --argstr git_version ${DRONE_TAG:-$DRONE_COMMIT} - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: integration @@ -158,7 +158,7 @@ steps: - name: build image: nixpkgs/nix:nixos-22.05 commands: - - nix-build --no-build-output --attr pkgs.arm64.release --argstr git_version $DRONE_COMMIT + - nix-build --no-build-output --attr pkgs.arm64.release --argstr git_version ${DRONE_TAG:-$DRONE_COMMIT} - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: push static binary @@ -203,7 +203,7 @@ steps: - name: build image: nixpkgs/nix:nixos-22.05 commands: - - nix-build --no-build-output --attr pkgs.arm.release --argstr git_version $DRONE_COMMIT + - nix-build --no-build-output --attr pkgs.arm.release --argstr git_version ${DRONE_TAG:-$DRONE_COMMIT} - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage" - name: push static binary @@ -269,6 +269,6 @@ trigger: --- kind: signature -hmac: ccec0e06bf6676a705d6a4e63dc322691148bc72e72a4aaa54be6c5fd22652a2 +hmac: fa1f98f327abf88486c0c54984287285a4b951efa3776af9dd33b4d782b50815 ... diff --git a/Cargo.lock b/Cargo.lock index e1ccfc2d..fb708a8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1176,7 +1176,6 @@ dependencies = [ "futures-util", "garage_util 0.7.0", "gethostname", - "git-version", "hex", "hyper", "k8s-openapi", @@ -1276,6 +1275,7 @@ dependencies = [ "err-derive 0.3.1", "futures", "garage_db", + "git-version", "hex", "http", "hyper", diff --git a/Cargo.nix b/Cargo.nix index 37bf3186..bb0d9bfa 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -55,7 +55,7 @@ in garage_api = rustPackages.unknown.garage_api."0.7.0"; garage_web = rustPackages.unknown.garage_web."0.7.0"; garage = rustPackages.unknown.garage."0.7.0"; - k2v-client = rustPackages.unknown.k2v-client."0.1.0"; + k2v-client = rustPackages.unknown.k2v-client."0.0.1"; }; "registry+https://github.com/rust-lang/crates.io-index".ahash."0.7.6" = overridableMkRustCrate (profileName: rec { name = "ahash"; @@ -749,7 +749,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -1625,7 +1625,6 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "gethostname" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".gethostname."0.2.3" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "git_version" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".git-version."0.3.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "hyper" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "k8s_openapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".k8s-openapi."0.13.1" { inherit profileName; }; @@ -1736,6 +1735,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "err_derive" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "garage_db" else null } = rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "git_version" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".git-version."0.3.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "http" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "hyper" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; @@ -2327,9 +2327,9 @@ in }; }); - "unknown".k2v-client."0.1.0" = overridableMkRustCrate (profileName: rec { + "unknown".k2v-client."0.0.1" = overridableMkRustCrate (profileName: rec { name = "k2v-client"; - version = "0.1.0"; + version = "0.0.1"; registry = "unknown"; src = fetchCrateLocal (workspaceSrc + "/src/k2v-client"); features = builtins.concatLists [ @@ -2756,7 +2756,7 @@ in [ "os-poll" ] ]; dependencies = { - ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "wasi" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "wasi" || hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if hostPlatform.isWindows then "miow" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".miow."0.3.7" { inherit profileName; }; ${ if hostPlatform.isWindows then "ntapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ntapi."0.3.7" { inherit profileName; }; @@ -4534,7 +4534,7 @@ in ]; dependencies = { bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot_core."0.8.5" { inherit profileName; }; static_init_macro = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init_macro."1.0.2" { profileName = "__noProfile"; }; @@ -5554,11 +5554,11 @@ in [ "default" ] ]; dependencies = { - ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-uwp-windows-gnu" || hostPlatform.config == "i686-pc-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); diff --git a/default.nix b/default.nix index 4d7558c5..7e44096c 100644 --- a/default.nix +++ b/default.nix @@ -9,8 +9,8 @@ let pkgs = import pkgsSrc { }; compile = import ./nix/compile.nix; build_debug_and_release = (target: { - debug = (compile { inherit target; release = false; }).workspace.garage { compileMode = "build"; }; - release = (compile { inherit target; release = true; }).workspace.garage { compileMode = "build"; }; + debug = (compile { inherit target git_version; release = false; }).workspace.garage { compileMode = "build"; }; + release = (compile { inherit target git_version; release = true; }).workspace.garage { compileMode = "build"; }; }); test = (rustPkgs: pkgs.symlinkJoin { name ="garage-tests"; @@ -25,9 +25,9 @@ in { arm = build_debug_and_release "armv6l-unknown-linux-musleabihf"; }; test = { - amd64 = test (compile { target = "x86_64-unknown-linux-musl"; }); + amd64 = test (compile { inherit git_version; target = "x86_64-unknown-linux-musl"; }); }; clippy = { - amd64 = (compile { compiler = "clippy"; }).workspace.garage { compileMode = "build"; } ; + amd64 = (compile { inherit git_version; compiler = "clippy"; }).workspace.garage { compileMode = "build"; } ; }; } diff --git a/nix/compile.nix b/nix/compile.nix index 49f7c1d6..7986fb0d 100644 --- a/nix/compile.nix +++ b/nix/compile.nix @@ -129,16 +129,7 @@ let (pkgs.rustBuilder.rustLib.makeOverride { name = "garage_rpc"; - overrideAttrs = drv: - (if git_version != null then { - /* [3] */ preConfigure = '' - ${drv.preConfigure or ""} - export GIT_VERSION="${git_version}" - ''; - } else {}) - // { - /* [1] */ setBuildEnv = (buildEnv drv); - }; + overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; overrideArgs = old: { /* [4] */ features = if release then [ "kubernetes-discovery" ] else []; }; @@ -151,7 +142,15 @@ let (pkgs.rustBuilder.rustLib.makeOverride { name = "garage_util"; - overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; + overrideAttrs = drv: + (if git_version != null then { + /* [3] */ preConfigure = '' + ${drv.preConfigure or ""} + export GIT_VERSION="${git_version}" + ''; + } else {}) + // + { /* [1] */ setBuildEnv = (buildEnv drv); }; }) (pkgs.rustBuilder.rustLib.makeOverride { diff --git a/src/garage/cli/structs.rs b/src/garage/cli/structs.rs index bc44b5ef..9274f80f 100644 --- a/src/garage/cli/structs.rs +++ b/src/garage/cli/structs.rs @@ -1,64 +1,65 @@ use serde::{Deserialize, Serialize}; +use garage_util::version; use structopt::StructOpt; #[derive(StructOpt, Debug)] pub enum Command { /// Run Garage server - #[structopt(name = "server")] + #[structopt(name = "server", version = version::garage())] Server, /// Get network status - #[structopt(name = "status")] + #[structopt(name = "status", version = version::garage())] Status, /// Operations on individual Garage nodes - #[structopt(name = "node")] + #[structopt(name = "node", version = version::garage())] Node(NodeOperation), /// Operations on the assignation of node roles in the cluster layout - #[structopt(name = "layout")] + #[structopt(name = "layout", version = version::garage())] Layout(LayoutOperation), /// Operations on buckets - #[structopt(name = "bucket")] + #[structopt(name = "bucket", version = version::garage())] Bucket(BucketOperation), /// Operations on S3 access keys - #[structopt(name = "key")] + #[structopt(name = "key", version = version::garage())] Key(KeyOperation), /// Run migrations from previous Garage version /// (DO NOT USE WITHOUT READING FULL DOCUMENTATION) - #[structopt(name = "migrate")] + #[structopt(name = "migrate", version = version::garage())] Migrate(MigrateOpt), /// Start repair of node data on remote node - #[structopt(name = "repair")] + #[structopt(name = "repair", version = version::garage())] Repair(RepairOpt), /// Offline reparation of node data (these repairs must be run offline /// directly on the server node) - #[structopt(name = "offline-repair")] + #[structopt(name = "offline-repair", version = version::garage())] OfflineRepair(OfflineRepairOpt), /// Gather node statistics - #[structopt(name = "stats")] + #[structopt(name = "stats", version = version::garage())] Stats(StatsOpt), /// Manage background workers - #[structopt(name = "worker")] + #[structopt(name = "worker", version = version::garage())] Worker(WorkerOpt), } #[derive(StructOpt, Debug)] pub enum NodeOperation { /// Print identifier (public key) of this Garage node - #[structopt(name = "id")] + #[structopt(name = "id", version = version::garage())] NodeId(NodeIdOpt), /// Connect to Garage node that is currently isolated from the system - #[structopt(name = "connect")] + #[structopt(name = "connect", version = version::garage())] Connect(ConnectNodeOpt), } @@ -79,23 +80,23 @@ pub struct ConnectNodeOpt { #[derive(StructOpt, Debug)] pub enum LayoutOperation { /// Assign role to Garage node - #[structopt(name = "assign")] + #[structopt(name = "assign", version = version::garage())] Assign(AssignRoleOpt), /// Remove role from Garage cluster node - #[structopt(name = "remove")] + #[structopt(name = "remove", version = version::garage())] Remove(RemoveRoleOpt), /// Show roles currently assigned to nodes and changes staged for commit - #[structopt(name = "show")] + #[structopt(name = "show", version = version::garage())] Show, /// Apply staged changes to cluster layout - #[structopt(name = "apply")] + #[structopt(name = "apply", version = version::garage())] Apply(ApplyLayoutOpt), /// Revert staged changes to cluster layout - #[structopt(name = "revert")] + #[structopt(name = "revert", version = version::garage())] Revert(RevertLayoutOpt), } @@ -150,43 +151,43 @@ pub struct RevertLayoutOpt { #[derive(Serialize, Deserialize, StructOpt, Debug)] pub enum BucketOperation { /// List buckets - #[structopt(name = "list")] + #[structopt(name = "list", version = version::garage())] List, /// Get bucket info - #[structopt(name = "info")] + #[structopt(name = "info", version = version::garage())] Info(BucketOpt), /// Create bucket - #[structopt(name = "create")] + #[structopt(name = "create", version = version::garage())] Create(BucketOpt), /// Delete bucket - #[structopt(name = "delete")] + #[structopt(name = "delete", version = version::garage())] Delete(DeleteBucketOpt), /// Alias bucket under new name - #[structopt(name = "alias")] + #[structopt(name = "alias", version = version::garage())] Alias(AliasBucketOpt), /// Remove bucket alias - #[structopt(name = "unalias")] + #[structopt(name = "unalias", version = version::garage())] Unalias(UnaliasBucketOpt), /// Allow key to read or write to bucket - #[structopt(name = "allow")] + #[structopt(name = "allow", version = version::garage())] Allow(PermBucketOpt), /// Deny key from reading or writing to bucket - #[structopt(name = "deny")] + #[structopt(name = "deny", version = version::garage())] Deny(PermBucketOpt), /// Expose as website or not - #[structopt(name = "website")] + #[structopt(name = "website", version = version::garage())] Website(WebsiteOpt), /// Set the quotas for this bucket - #[structopt(name = "set-quotas")] + #[structopt(name = "set-quotas", version = version::garage())] SetQuotas(SetQuotasOpt), } @@ -292,35 +293,35 @@ pub struct SetQuotasOpt { #[derive(Serialize, Deserialize, StructOpt, Debug)] pub enum KeyOperation { /// List keys - #[structopt(name = "list")] + #[structopt(name = "list", version = version::garage())] List, /// Get key info - #[structopt(name = "info")] + #[structopt(name = "info", version = version::garage())] Info(KeyOpt), /// Create new key - #[structopt(name = "new")] + #[structopt(name = "new", version = version::garage())] New(KeyNewOpt), /// Rename key - #[structopt(name = "rename")] + #[structopt(name = "rename", version = version::garage())] Rename(KeyRenameOpt), /// Delete key - #[structopt(name = "delete")] + #[structopt(name = "delete", version = version::garage())] Delete(KeyDeleteOpt), /// Set permission flags for key - #[structopt(name = "allow")] + #[structopt(name = "allow", version = version::garage())] Allow(KeyPermOpt), /// Unset permission flags for key - #[structopt(name = "deny")] + #[structopt(name = "deny", version = version::garage())] Deny(KeyPermOpt), /// Import key - #[structopt(name = "import")] + #[structopt(name = "import", version = version::garage())] Import(KeyImportOpt), } @@ -392,7 +393,7 @@ pub struct MigrateOpt { #[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)] pub enum MigrateWhat { /// Migrate buckets and permissions from v0.5.0 - #[structopt(name = "buckets050")] + #[structopt(name = "buckets050", version = version::garage())] Buckets050, } @@ -413,19 +414,19 @@ pub struct RepairOpt { #[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)] pub enum RepairWhat { /// Only do a full sync of metadata tables - #[structopt(name = "tables")] + #[structopt(name = "tables", version = version::garage())] Tables, /// Only repair (resync/rebalance) the set of stored blocks - #[structopt(name = "blocks")] + #[structopt(name = "blocks", version = version::garage())] Blocks, /// Only redo the propagation of object deletions to the version table (slow) - #[structopt(name = "versions")] + #[structopt(name = "versions", version = version::garage())] Versions, /// Only redo the propagation of version deletions to the block ref table (extremely slow) - #[structopt(name = "block_refs")] + #[structopt(name = "block_refs", version = version::garage())] BlockRefs, /// Verify integrity of all blocks on disc (extremely slow, i/o intensive) - #[structopt(name = "scrub")] + #[structopt(name = "scrub", version = version::garage())] Scrub { #[structopt(subcommand)] cmd: ScrubCmd, @@ -435,19 +436,19 @@ pub enum RepairWhat { #[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)] pub enum ScrubCmd { /// Start scrub - #[structopt(name = "start")] + #[structopt(name = "start", version = version::garage())] Start, /// Pause scrub (it will resume automatically after 24 hours) - #[structopt(name = "pause")] + #[structopt(name = "pause", version = version::garage())] Pause, /// Resume paused scrub - #[structopt(name = "resume")] + #[structopt(name = "resume", version = version::garage())] Resume, /// Cancel scrub in progress - #[structopt(name = "cancel")] + #[structopt(name = "cancel", version = version::garage())] Cancel, /// Set tranquility level for in-progress and future scrubs - #[structopt(name = "set-tranquility")] + #[structopt(name = "set-tranquility", version = version::garage())] SetTranquility { #[structopt()] tranquility: u32, @@ -468,10 +469,10 @@ pub struct OfflineRepairOpt { pub enum OfflineRepairWhat { /// Repair K2V item counters #[cfg(feature = "k2v")] - #[structopt(name = "k2v_item_counters")] + #[structopt(name = "k2v_item_counters", version = version::garage())] K2VItemCounters, /// Repair object counters - #[structopt(name = "object_counters")] + #[structopt(name = "object_counters", version = version::garage())] ObjectCounters, } @@ -495,7 +496,7 @@ pub struct WorkerOpt { #[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)] pub enum WorkerCmd { /// List all workers on Garage node - #[structopt(name = "list")] + #[structopt(name = "list", version = version::garage())] List { #[structopt(flatten)] opt: WorkerListOpt, diff --git a/src/garage/main.rs b/src/garage/main.rs index 3fa5c3c0..89888884 100644 --- a/src/garage/main.rs +++ b/src/garage/main.rs @@ -22,6 +22,7 @@ use garage_util::error::*; use garage_rpc::system::*; use garage_rpc::*; +use garage_util::version; use garage_model::helper::error::Error as HelperError; @@ -29,7 +30,7 @@ use admin::*; use cli::*; #[derive(StructOpt, Debug)] -#[structopt(name = "garage")] +#[structopt(name = "garage", version = version::garage(), about = "S3-compatible object store for self-hosted geo-distributed deployments")] struct Opt { /// Host to connect to for admin operations, in the format: /// @: diff --git a/src/rpc/Cargo.toml b/src/rpc/Cargo.toml index 73328993..80a1975c 100644 --- a/src/rpc/Cargo.toml +++ b/src/rpc/Cargo.toml @@ -19,7 +19,6 @@ garage_util = { version = "0.7.0", path = "../util" } arc-swap = "1.0" bytes = "1.0" gethostname = "0.2" -git-version = "0.3.4" hex = "0.4" tracing = "0.1.30" rand = "0.8" diff --git a/src/rpc/system.rs b/src/rpc/system.rs index f9f2970b..fbfbbf56 100644 --- a/src/rpc/system.rs +++ b/src/rpc/system.rs @@ -27,6 +27,7 @@ use garage_util::data::*; use garage_util::error::*; use garage_util::persister::Persister; use garage_util::time::*; +use garage_util::version; use crate::consul::*; #[cfg(feature = "kubernetes-discovery")] @@ -320,11 +321,7 @@ impl System { // also available through RPC) ---- pub fn garage_version(&self) -> &'static str { - option_env!("GIT_VERSION").unwrap_or(git_version::git_version!( - prefix = "git:", - cargo_prefix = "cargo:", - fallback = "unknown" - )) + version::garage() } pub fn get_known_nodes(&self) -> Vec { diff --git a/src/util/Cargo.toml b/src/util/Cargo.toml index 57c70ffb..783fb3fc 100644 --- a/src/util/Cargo.toml +++ b/src/util/Cargo.toml @@ -24,6 +24,7 @@ hex = "0.4" tracing = "0.1.30" rand = "0.8" sha2 = "0.9" +git-version = "0.3.4" chrono = "0.4" rmp-serde = "0.15" @@ -43,5 +44,6 @@ hyper = "0.14" opentelemetry = { version = "0.17", features = [ "rt-tokio", "metrics", "trace" ] } + [features] k2v = [] diff --git a/src/util/lib.rs b/src/util/lib.rs index fce151af..47c85c3a 100644 --- a/src/util/lib.rs +++ b/src/util/lib.rs @@ -14,3 +14,4 @@ pub mod persister; pub mod time; pub mod token_bucket; pub mod tranquilizer; +pub mod version; diff --git a/src/util/version.rs b/src/util/version.rs new file mode 100644 index 00000000..8882d035 --- /dev/null +++ b/src/util/version.rs @@ -0,0 +1,7 @@ +pub fn garage() -> &'static str { + option_env!("GIT_VERSION").unwrap_or(git_version::git_version!( + prefix = "git:", + cargo_prefix = "cargo:", + fallback = "unknown" + )) +} From 532eca7ff94e4710283fb38951a349a83654de59 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 10 Aug 2022 18:28:12 +0200 Subject: [PATCH 060/149] Add some documentation for Caddy --- doc/book/cookbook/reverse-proxy.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/doc/book/cookbook/reverse-proxy.md b/doc/book/cookbook/reverse-proxy.md index 27add5bf..fb918778 100644 --- a/doc/book/cookbook/reverse-proxy.md +++ b/doc/book/cookbook/reverse-proxy.md @@ -280,3 +280,25 @@ Traefik's caching middleware is only available on [entreprise version](https://d [http.middlewares] [http.middlewares.gzip_compress.compress] ``` + +## Caddy + +Your Caddy configuration can be as simple as: + +```caddy +s3.garage.tld, *.s3.garage.tld { + reverse_proxy localhost:3900 192.168.1.2:3900 example.tld:3900 +} + +*.web.garage.tld { + reverse_proxy localhost:3902 192.168.1.2:3900 example.tld:3900 +} + +admin.garage.tld { + reverse_proxy localhost:3903 +} +``` + +But at the same time, the `reverse_proxy` is very flexible. +For a production deployment, you should [read its documentation](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy) as it supports features like DNS discovery of upstreams, load balancing with checks, streaming parameters, etc. + From 4da67b00351ba30226de64bcc923d03608461063 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 29 Aug 2022 16:48:31 +0200 Subject: [PATCH 061/149] Update drone signature --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index ce576278..277bfbc2 100644 --- a/.drone.yml +++ b/.drone.yml @@ -269,6 +269,6 @@ trigger: --- kind: signature -hmac: fa1f98f327abf88486c0c54984287285a4b951efa3776af9dd33b4d782b50815 +hmac: 362639b4c9541ad9bd06ff7f72b5235b2b0216bcb16eececd25285b6fe94ba6f ... From 5d065b8a0f66d7898746b4b419a1a55cfccc9bbf Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 29 Aug 2022 17:24:53 +0200 Subject: [PATCH 062/149] cargo2nix fix to fetchCrateGit --- nix/common.nix | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/common.nix b/nix/common.nix index 8396a6de..aa59cdc0 100644 --- a/nix/common.nix +++ b/nix/common.nix @@ -8,10 +8,10 @@ rec { sha256 = "1xy9zpypqfxs5gcq5dcla4bfkhxmh5nzn9dyqkr03lqycm9wg5cr"; }; cargo2nixSrc = fetchGit { - # As of 2022-03-17 - url = "https://github.com/superboum/cargo2nix"; - ref = "dedup_propagate"; - rev = "486675c67249e735dd7eb68e1b9feac9db102be7"; + # As of 2022-08-29, stacking two patches: superboum@dedup_propagate and Alexis211@fix_fetchcrategit + url = "https://github.com/Alexis211/cargo2nix"; + ref = "fix_fetchcrategit"; + rev = "4b31c0cc05b6394916d46e9289f51263d81973b9"; }; /* From 322dafc761295df45c081183c5fc059a750a3249 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 29 Aug 2022 17:32:45 +0200 Subject: [PATCH 063/149] Try to fix clippy --- src/rpc/rpc_helper.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rpc/rpc_helper.rs b/src/rpc/rpc_helper.rs index ddabd636..216fffd4 100644 --- a/src/rpc/rpc_helper.rs +++ b/src/rpc/rpc_helper.rs @@ -404,7 +404,7 @@ impl RpcHelper { let mut nodes = nodes .iter() .map(|to| { - let peer_zone = match ring.layout.node_role(&to) { + let peer_zone = match ring.layout.node_role(to) { Some(pc) => &pc.zone, None => "", }; From dd5304f6fc2b4af3556549a3b587f588407dfa71 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 31 Aug 2022 14:24:41 +0200 Subject: [PATCH 064/149] Replace logging crate pretty_env_logger by tracing_subscriber::fmt --- Cargo.lock | 61 +++++++++++++++++++++++++- Cargo.nix | 100 +++++++++++++++++++++++++++++++++++++++--- src/garage/Cargo.toml | 2 +- src/garage/main.rs | 2 +- 4 files changed, 156 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 153643ce..f9169421 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,6 +22,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anyhow" version = "1.0.56" @@ -1004,7 +1013,6 @@ dependencies = [ "opentelemetry", "opentelemetry-otlp", "opentelemetry-prometheus", - "pretty_env_logger", "prometheus", "rand 0.8.5", "rmp-serde 0.15.5", @@ -1018,6 +1026,7 @@ dependencies = [ "tokio", "toml", "tracing", + "tracing-subscriber", ] [[package]] @@ -3294,6 +3303,15 @@ dependencies = [ "digest 0.10.3", ] +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.1.0" @@ -3542,6 +3560,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + [[package]] name = "time" version = "0.1.44" @@ -3824,6 +3851,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa31669fa42c09c34d94d8165dd2012e8ff3c66aca50f3bb226b68f216f2706c" dependencies = [ "lazy_static", + "valuable", ] [[package]] @@ -3836,6 +3864,31 @@ dependencies = [ "tracing", ] +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bc28f93baff38037f64e6f43d34cfa1605f27a49c34e8a04c5e78b0babf2596" +dependencies = [ + "ansi_term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + [[package]] name = "treediff" version = "3.0.2" @@ -3917,6 +3970,12 @@ dependencies = [ "getrandom", ] +[[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" diff --git a/Cargo.nix b/Cargo.nix index edbe1134..4130e550 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -85,6 +85,16 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".ansi_term."0.12.1" = overridableMkRustCrate (profileName: rec { + name = "ansi_term"; + version = "0.12.1"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"; }; + dependencies = { + ${ if hostPlatform.parsed.kernel.name == "windows" then "winapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".winapi."0.3.9" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".anyhow."1.0.56" = overridableMkRustCrate (profileName: rec { name = "anyhow"; version = "1.0.56"; @@ -1421,7 +1431,6 @@ in opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; opentelemetry_otlp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-otlp."0.10.0" { inherit profileName; }; opentelemetry_prometheus = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-prometheus."0.10.0" { inherit profileName; }; - pretty_env_logger = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pretty_env_logger."0.4.0" { inherit profileName; }; prometheus = rustPackages."registry+https://github.com/rust-lang/crates.io-index".prometheus."0.13.0" { inherit profileName; }; rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; @@ -1432,6 +1441,7 @@ in tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; toml = rustPackages."registry+https://github.com/rust-lang/crates.io-index".toml."0.5.8" { inherit profileName; }; tracing = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; + tracing_subscriber = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-subscriber."0.3.11" { inherit profileName; }; }; devDependencies = { assert_json_diff = rustPackages."registry+https://github.com/rust-lang/crates.io-index".assert-json-diff."2.0.1" { inherit profileName; }; @@ -2808,7 +2818,7 @@ in [ "os-poll" ] ]; dependencies = { - ${ if hostPlatform.parsed.kernel.name == "wasi" || hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "wasi" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if hostPlatform.isWindows then "miow" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".miow."0.3.7" { inherit profileName; }; ${ if hostPlatform.isWindows then "ntapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ntapi."0.3.7" { inherit profileName; }; @@ -4460,6 +4470,16 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".sharded-slab."0.1.4" = overridableMkRustCrate (profileName: rec { + name = "sharded-slab"; + version = "0.1.4"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"; }; + dependencies = { + lazy_static = rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".shlex."1.1.0" = overridableMkRustCrate (profileName: rec { name = "shlex"; version = "1.1.0"; @@ -4785,6 +4805,16 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".thread_local."1.1.4" = overridableMkRustCrate (profileName: rec { + name = "thread_local"; + version = "1.1.4"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"; }; + dependencies = { + once_cell = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".time."0.1.44" = overridableMkRustCrate (profileName: rec { name = "time"; version = "0.1.44"; @@ -5209,11 +5239,14 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "aa31669fa42c09c34d94d8165dd2012e8ff3c66aca50f3bb226b68f216f2706c"; }; features = builtins.concatLists [ + (lib.optional (rootFeatures' ? "garage") "default") [ "lazy_static" ] [ "std" ] + (lib.optional (rootFeatures' ? "garage") "valuable") ]; dependencies = { lazy_static = rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { inherit profileName; }; + ${ if false then "valuable" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".valuable."0.1.0" { inherit profileName; }; }; }); @@ -5234,6 +5267,50 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".tracing-log."0.1.3" = overridableMkRustCrate (profileName: rec { + name = "tracing-log"; + version = "0.1.3"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"; }; + features = builtins.concatLists [ + [ "log-tracer" ] + [ "std" ] + ]; + dependencies = { + lazy_static = rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { inherit profileName; }; + log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; + tracing_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-core."0.1.23" { inherit profileName; }; + }; + }); + + "registry+https://github.com/rust-lang/crates.io-index".tracing-subscriber."0.3.11" = overridableMkRustCrate (profileName: rec { + name = "tracing-subscriber"; + version = "0.3.11"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "4bc28f93baff38037f64e6f43d34cfa1605f27a49c34e8a04c5e78b0babf2596"; }; + features = builtins.concatLists [ + [ "alloc" ] + [ "ansi" ] + [ "ansi_term" ] + [ "default" ] + [ "fmt" ] + [ "registry" ] + [ "sharded-slab" ] + [ "smallvec" ] + [ "std" ] + [ "thread_local" ] + [ "tracing-log" ] + ]; + dependencies = { + ansi_term = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ansi_term."0.12.1" { inherit profileName; }; + sharded_slab = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sharded-slab."0.1.4" { inherit profileName; }; + smallvec = rustPackages."registry+https://github.com/rust-lang/crates.io-index".smallvec."1.8.0" { inherit profileName; }; + thread_local = rustPackages."registry+https://github.com/rust-lang/crates.io-index".thread_local."1.1.4" { inherit profileName; }; + tracing_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-core."0.1.23" { inherit profileName; }; + tracing_log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-log."0.1.3" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".treediff."3.0.2" = overridableMkRustCrate (profileName: rec { name = "treediff"; version = "3.0.2"; @@ -5350,6 +5427,17 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".valuable."0.1.0" = overridableMkRustCrate (profileName: rec { + name = "valuable"; + version = "0.1.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"; }; + features = builtins.concatLists [ + [ "alloc" ] + [ "std" ] + ]; + }); + "registry+https://github.com/rust-lang/crates.io-index".vcpkg."0.2.15" = overridableMkRustCrate (profileName: rec { name = "vcpkg"; version = "0.2.15"; @@ -5622,11 +5710,11 @@ in [ "default" ] ]; dependencies = { - ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-uwp-windows-gnu" || hostPlatform.config == "i686-pc-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index 2cb8ec46..6eb4c5d2 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -35,7 +35,7 @@ bytesize = "1.1" timeago = "0.3" hex = "0.4" tracing = { version = "0.1.30", features = ["log-always"] } -pretty_env_logger = "0.4" +tracing-subscriber = "0.3" rand = "0.8" async-trait = "0.1.7" sodiumoxide = { version = "0.2.5-0", package = "kuska-sodiumoxide" } diff --git a/src/garage/main.rs b/src/garage/main.rs index 89888884..65abfd48 100644 --- a/src/garage/main.rs +++ b/src/garage/main.rs @@ -59,7 +59,7 @@ async fn main() { if std::env::var("RUST_LOG").is_err() { std::env::set_var("RUST_LOG", "netapp=info,garage=info") } - pretty_env_logger::init(); + tracing_subscriber::fmt::init(); sodiumoxide::init().expect("Unable to init sodiumoxide"); // Abort on panic (same behavior as in Go) From 44cd98d2e4eb981f29c3124c7ab3ddf55ccb3848 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 31 Aug 2022 14:28:17 +0200 Subject: [PATCH 065/149] Tracing-subscriber: write to stderr --- src/garage/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/garage/main.rs b/src/garage/main.rs index 65abfd48..cc441727 100644 --- a/src/garage/main.rs +++ b/src/garage/main.rs @@ -59,7 +59,9 @@ async fn main() { if std::env::var("RUST_LOG").is_err() { std::env::set_var("RUST_LOG", "netapp=info,garage=info") } - tracing_subscriber::fmt::init(); + tracing_subscriber::fmt() + .with_writer(std::io::stderr) + .init(); sodiumoxide::init().expect("Unable to init sodiumoxide"); // Abort on panic (same behavior as in Go) From efbca67ce43891f4cfe696bbd182f6726e8fdc73 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 31 Aug 2022 14:39:12 +0200 Subject: [PATCH 066/149] Add env filter to tracing subscriber --- Cargo.lock | 22 ++++++++++++++++++++++ src/garage/Cargo.toml | 2 +- src/garage/main.rs | 1 + 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index f9169421..96a8cc61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1993,6 +1993,15 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + [[package]] name = "matches" version = "0.1.9" @@ -2916,6 +2925,15 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.6.25" @@ -3882,9 +3900,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bc28f93baff38037f64e6f43d34cfa1605f27a49c34e8a04c5e78b0babf2596" dependencies = [ "ansi_term", + "lazy_static", + "matchers", + "regex", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index 6eb4c5d2..4de377aa 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -35,7 +35,7 @@ bytesize = "1.1" timeago = "0.3" hex = "0.4" tracing = { version = "0.1.30", features = ["log-always"] } -tracing-subscriber = "0.3" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } rand = "0.8" async-trait = "0.1.7" sodiumoxide = { version = "0.2.5-0", package = "kuska-sodiumoxide" } diff --git a/src/garage/main.rs b/src/garage/main.rs index cc441727..f6e694f3 100644 --- a/src/garage/main.rs +++ b/src/garage/main.rs @@ -61,6 +61,7 @@ async fn main() { } tracing_subscriber::fmt() .with_writer(std::io::stderr) + .with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env()) .init(); sodiumoxide::init().expect("Unable to init sodiumoxide"); From eb97e13a6a02106520665011f5d30b60e37b730e Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 31 Aug 2022 17:42:00 +0200 Subject: [PATCH 067/149] update cargo.nix --- Cargo.nix | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/Cargo.nix b/Cargo.nix index 4130e550..97afbd64 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -769,7 +769,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -2717,6 +2717,16 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".matchers."0.1.0" = overridableMkRustCrate (profileName: rec { + name = "matchers"; + version = "0.1.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"; }; + dependencies = { + regex_automata = rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex-automata."0.1.10" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".matches."0.1.9" = overridableMkRustCrate (profileName: rec { name = "matches"; version = "0.1.9"; @@ -2818,7 +2828,7 @@ in [ "os-poll" ] ]; dependencies = { - ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "wasi" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "wasi" || hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if hostPlatform.isWindows then "miow" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".miow."0.3.7" { inherit profileName; }; ${ if hostPlatform.isWindows then "ntapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ntapi."0.3.7" { inherit profileName; }; @@ -3959,6 +3969,21 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".regex-automata."0.1.10" = overridableMkRustCrate (profileName: rec { + name = "regex-automata"; + version = "0.1.10"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"; }; + features = builtins.concatLists [ + [ "default" ] + [ "regex-syntax" ] + [ "std" ] + ]; + dependencies = { + regex_syntax = rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex-syntax."0.6.25" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".regex-syntax."0.6.25" = overridableMkRustCrate (profileName: rec { name = "regex-syntax"; version = "0.6.25"; @@ -4622,7 +4647,7 @@ in ]; dependencies = { bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot_core."0.8.5" { inherit profileName; }; static_init_macro = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init_macro."1.0.2" { profileName = "__noProfile"; }; @@ -5293,19 +5318,28 @@ in [ "ansi" ] [ "ansi_term" ] [ "default" ] + [ "env-filter" ] [ "fmt" ] + [ "lazy_static" ] + [ "matchers" ] + [ "regex" ] [ "registry" ] [ "sharded-slab" ] [ "smallvec" ] [ "std" ] [ "thread_local" ] + [ "tracing" ] [ "tracing-log" ] ]; dependencies = { ansi_term = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ansi_term."0.12.1" { inherit profileName; }; + lazy_static = rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { inherit profileName; }; + matchers = rustPackages."registry+https://github.com/rust-lang/crates.io-index".matchers."0.1.0" { inherit profileName; }; + regex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex."1.5.5" { inherit profileName; }; sharded_slab = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sharded-slab."0.1.4" { inherit profileName; }; smallvec = rustPackages."registry+https://github.com/rust-lang/crates.io-index".smallvec."1.8.0" { inherit profileName; }; thread_local = rustPackages."registry+https://github.com/rust-lang/crates.io-index".thread_local."1.1.4" { inherit profileName; }; + tracing = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; tracing_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-core."0.1.23" { inherit profileName; }; tracing_log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-log."0.1.3" { inherit profileName; }; }; @@ -5713,7 +5747,7 @@ in ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-uwp-windows-gnu" || hostPlatform.config == "i686-pc-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); From e598231ca442715af8321ff5520e5b3d59609ed9 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 31 Aug 2022 19:27:25 +0200 Subject: [PATCH 068/149] update netapp git commit --- Cargo.lock | 2 +- Cargo.nix | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e3bebfb8..cfc92d21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2176,7 +2176,7 @@ dependencies = [ [[package]] name = "netapp" version = "0.5.0" -source = "git+https://git.deuxfleurs.fr/lx/netapp?branch=stream-body#74e57016f63b6052cf6d539812859c3a46138eee" +source = "git+https://git.deuxfleurs.fr/lx/netapp?branch=stream-body#b55f61c38b01da01314d99ced543aba713dbd2a9" dependencies = [ "arc-swap", "async-trait", diff --git a/Cargo.nix b/Cargo.nix index 6d35fb9e..7760ee1f 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -2965,7 +2965,7 @@ in url = https://git.deuxfleurs.fr/lx/netapp; name = "netapp"; version = "0.5.0"; - rev = "74e57016f63b6052cf6d539812859c3a46138eee"; + rev = "b55f61c38b01da01314d99ced543aba713dbd2a9"; ref = "stream-body";}; features = builtins.concatLists [ [ "default" ] @@ -4055,7 +4055,7 @@ in ]; dependencies = { ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" || hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "i686" || hostPlatform.parsed.cpu.name == "x86_64" || (hostPlatform.parsed.cpu.name == "aarch64" || hostPlatform.parsed.cpu.name == "armv6l" || hostPlatform.parsed.cpu.name == "armv7l") && (hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "fuchsia" || hostPlatform.parsed.kernel.name == "linux") then "spin" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".spin."0.5.2" { inherit profileName; }; untrusted = rustPackages."registry+https://github.com/rust-lang/crates.io-index".untrusted."0.7.1" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "wasm32" && hostPlatform.parsed.vendor.name == "unknown" && hostPlatform.parsed.kernel.name == "unknown" && hostPlatform.parsed.abi.name == "" then "web_sys" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.56" { inherit profileName; }; @@ -4677,7 +4677,7 @@ in ]; dependencies = { bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot_core."0.8.5" { inherit profileName; }; static_init_macro = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init_macro."1.0.2" { profileName = "__noProfile"; }; @@ -5774,11 +5774,11 @@ in [ "default" ] ]; dependencies = { - ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); From 70231d68b27054c2185b73b5ceee1c445baaaa2d Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 31 Aug 2022 19:44:27 +0200 Subject: [PATCH 069/149] Fix bytes_read counter --- src/block/manager.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/block/manager.rs b/src/block/manager.rs index 80c52510..b8fe4c74 100644 --- a/src/block/manager.rs +++ b/src/block/manager.rs @@ -449,8 +449,6 @@ impl BlockManager { let (header, data) = block.into_parts(); - self.metrics.bytes_read.add(data.len() as u64); - Resp::new(Ok(BlockRpc::PutBlock { hash: *hash, header, @@ -460,9 +458,16 @@ impl BlockManager { /// Read block from disk, verifying it's integrity pub(crate) async fn read_block(&self, hash: &Hash) -> Result { - self.read_block_internal(hash) + let data = self + .read_block_internal(hash) .bound_record_duration(&self.metrics.block_read_duration) - .await + .await?; + + self.metrics + .bytes_read + .add(data.inner_buffer().len() as u64); + + Ok(data) } async fn read_block_internal(&self, hash: &Hash) -> Result { From 4b726b09410b3b5ea9cde80d2a445d7914432e3c Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Thu, 1 Sep 2022 09:47:28 +0200 Subject: [PATCH 070/149] netapp recv with unbounded channel removes deadlock --- Cargo.lock | 2 +- Cargo.nix | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cfc92d21..7b5f6984 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2176,7 +2176,7 @@ dependencies = [ [[package]] name = "netapp" version = "0.5.0" -source = "git+https://git.deuxfleurs.fr/lx/netapp?branch=stream-body#b55f61c38b01da01314d99ced543aba713dbd2a9" +source = "git+https://git.deuxfleurs.fr/lx/netapp?branch=stream-body#3fd30c6e280fba41377c8b563352d756e8bc1caf" dependencies = [ "arc-swap", "async-trait", diff --git a/Cargo.nix b/Cargo.nix index 7760ee1f..79f6ca7e 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -791,7 +791,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.config == "aarch64-linux-android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -2965,7 +2965,7 @@ in url = https://git.deuxfleurs.fr/lx/netapp; name = "netapp"; version = "0.5.0"; - rev = "b55f61c38b01da01314d99ced543aba713dbd2a9"; + rev = "3fd30c6e280fba41377c8b563352d756e8bc1caf"; ref = "stream-body";}; features = builtins.concatLists [ [ "default" ] @@ -5776,9 +5776,9 @@ in dependencies = { ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); From bc977f9a7a7a5bd87ccf5fe96d64b397591f8ba0 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Thu, 1 Sep 2022 12:58:20 +0200 Subject: [PATCH 071/149] Update to Netapp with OrderTag support and exploit OrderTags --- Cargo.lock | 2 +- src/api/s3/copy.rs | 10 ++++++-- src/api/s3/get.rs | 21 ++++++++++++----- src/block/manager.rs | 55 ++++++++++++++++++++++++++++++------------- src/rpc/rpc_helper.rs | 2 +- 5 files changed, 64 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7b5f6984..bcb8f215 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2176,7 +2176,7 @@ dependencies = [ [[package]] name = "netapp" version = "0.5.0" -source = "git+https://git.deuxfleurs.fr/lx/netapp?branch=stream-body#3fd30c6e280fba41377c8b563352d756e8bc1caf" +source = "git+https://git.deuxfleurs.fr/lx/netapp?branch=stream-body#4a59b73d7bfd0f136f654e874afb5d2a9bf4df2e" dependencies = [ "arc-swap", "async-trait", diff --git a/src/api/s3/copy.rs b/src/api/s3/copy.rs index b54cbd23..10cf5935 100644 --- a/src/api/s3/copy.rs +++ b/src/api/s3/copy.rs @@ -9,6 +9,7 @@ use bytes::Bytes; use hyper::{Body, Request, Response}; use serde::Serialize; +use garage_rpc::rpc_helper::OrderTag; use garage_table::*; use garage_util::data::*; use garage_util::time::*; @@ -306,11 +307,16 @@ pub async fn handle_upload_part_copy( // if and only if the block returned is a block that already existed // in the Garage data store (thus we don't need to save it again). let garage2 = garage.clone(); + let order_stream = OrderTag::stream(); let source_blocks = stream::iter(blocks_to_copy) - .flat_map(|(block_hash, range_to_copy)| { + .enumerate() + .flat_map(|(i, (block_hash, range_to_copy))| { let garage3 = garage2.clone(); stream::once(async move { - let data = garage3.block_manager.rpc_get_block(&block_hash).await?; + let data = garage3 + .block_manager + .rpc_get_block(&block_hash, Some(order_stream.order(i as u64))) + .await?; match range_to_copy { Some(r) => Ok((data.slice(r), None)), None => Ok((data, Some(block_hash))), diff --git a/src/api/s3/get.rs b/src/api/s3/get.rs index c7621ade..dfc284fe 100644 --- a/src/api/s3/get.rs +++ b/src/api/s3/get.rs @@ -10,6 +10,7 @@ use http::header::{ use hyper::body::Bytes; use hyper::{Body, Request, Response, StatusCode}; +use garage_rpc::rpc_helper::OrderTag; use garage_table::EmptyKey; use garage_util::data::*; @@ -242,9 +243,11 @@ pub async fn handle_get( Ok(resp_builder.body(body)?) } ObjectVersionData::FirstBlock(_, first_block_hash) => { + let order_stream = OrderTag::stream(); + let read_first_block = garage .block_manager - .rpc_get_block_streaming(first_block_hash); + .rpc_get_block_streaming(first_block_hash, Some(order_stream.order(0))); let get_next_blocks = garage.version_table.get(&last_v.uuid, &EmptyKey); let (first_block_stream, version) = @@ -260,7 +263,8 @@ pub async fn handle_get( blocks[0].1 = Some(first_block_stream); let body_stream = futures::stream::iter(blocks) - .map(move |(hash, stream_opt)| { + .enumerate() + .map(move |(i, (hash, stream_opt))| { let garage = garage.clone(); async move { if let Some(stream) = stream_opt { @@ -268,7 +272,7 @@ pub async fn handle_get( } else { garage .block_manager - .rpc_get_block_streaming(&hash) + .rpc_get_block_streaming(&hash, Some(order_stream.order(i as u64))) .await .unwrap_or_else(|_| { Box::pin(futures::stream::once(async move { @@ -281,7 +285,7 @@ pub async fn handle_get( } } }) - .buffered(3) + .buffered(2) .flatten(); let body = hyper::body::Body::wrap_stream(body_stream); @@ -445,11 +449,16 @@ fn body_from_blocks_range( true_offset += b.size; } + let order_stream = OrderTag::stream(); let body_stream = futures::stream::iter(blocks) - .map(move |(block, true_offset)| { + .enumerate() + .map(move |(i, (block, true_offset))| { let garage = garage.clone(); async move { - let data = garage.block_manager.rpc_get_block(&block.hash).await?; + let data = garage + .block_manager + .rpc_get_block(&block.hash, Some(order_stream.order(i as u64))) + .await?; let start_in_block = if true_offset > begin { 0 } else { diff --git a/src/block/manager.rs b/src/block/manager.rs index b8fe4c74..b9f6fc0f 100644 --- a/src/block/manager.rs +++ b/src/block/manager.rs @@ -33,6 +33,7 @@ use garage_util::metrics::RecordDuration; use garage_util::time::*; use garage_util::tranquilizer::Tranquilizer; +use garage_rpc::rpc_helper::OrderTag; use garage_rpc::system::System; use garage_rpc::*; @@ -70,7 +71,7 @@ pub(crate) const BLOCK_GC_DELAY: Duration = Duration::from_secs(600); pub enum BlockRpc { Ok, /// Message to ask for a block of data, by hash - GetBlock(Hash), + GetBlock(Hash, Option), /// Message to send a block of data, either because requested, of for first delivery of new /// block PutBlock { @@ -183,15 +184,18 @@ impl BlockManager { async fn rpc_get_raw_block_streaming( &self, hash: &Hash, + order_tag: Option, ) -> Result<(DataBlockHeader, ByteStream), Error> { let who = self.replication.read_nodes(hash); //let who = self.system.rpc.request_order(&who); for node in who.iter() { let node_id = NodeID::from(*node); - let rpc = - self.endpoint - .call_streaming(&node_id, BlockRpc::GetBlock(*hash), PRIO_NORMAL); + let rpc = self.endpoint.call_streaming( + &node_id, + BlockRpc::GetBlock(*hash, order_tag), + PRIO_NORMAL, + ); tokio::select! { res = rpc => { let res = match res { @@ -224,15 +228,21 @@ impl BlockManager { /// Ask nodes that might have a (possibly compressed) block for it /// Return its entire body - async fn rpc_get_raw_block(&self, hash: &Hash) -> Result { + async fn rpc_get_raw_block( + &self, + hash: &Hash, + order_tag: Option, + ) -> Result { let who = self.replication.read_nodes(hash); //let who = self.system.rpc.request_order(&who); for node in who.iter() { let node_id = NodeID::from(*node); - let rpc = - self.endpoint - .call_streaming(&node_id, BlockRpc::GetBlock(*hash), PRIO_NORMAL); + let rpc = self.endpoint.call_streaming( + &node_id, + BlockRpc::GetBlock(*hash, order_tag), + PRIO_NORMAL, + ); tokio::select! { res = rpc => { let res = match res { @@ -275,11 +285,12 @@ impl BlockManager { pub async fn rpc_get_block_streaming( &self, hash: &Hash, + order_tag: Option, ) -> Result< Pin> + Send + Sync + 'static>>, Error, > { - let (header, stream) = self.rpc_get_raw_block_streaming(hash).await?; + let (header, stream) = self.rpc_get_raw_block_streaming(hash, order_tag).await?; match header { DataBlockHeader::Plain => Ok(Box::pin(stream.map_err(|_| { std::io::Error::new(std::io::ErrorKind::Other, "netapp stream error") @@ -295,8 +306,14 @@ impl BlockManager { } /// Ask nodes that might have a block for it - pub async fn rpc_get_block(&self, hash: &Hash) -> Result { - self.rpc_get_raw_block(hash).await?.verify_get(*hash) + pub async fn rpc_get_block( + &self, + hash: &Hash, + order_tag: Option, + ) -> Result { + self.rpc_get_raw_block(hash, order_tag) + .await? + .verify_get(*hash) } /// Send block to nodes that should have it @@ -441,7 +458,7 @@ impl BlockManager { Ok(()) } - async fn handle_get_block(&self, hash: &Hash) -> Resp { + async fn handle_get_block(&self, hash: &Hash, order_tag: Option) -> Resp { let block = match self.read_block(hash).await { Ok(data) => data, Err(e) => return Resp::new(Err(e)), @@ -449,11 +466,17 @@ impl BlockManager { let (header, data) = block.into_parts(); - Resp::new(Ok(BlockRpc::PutBlock { + let resp = Resp::new(Ok(BlockRpc::PutBlock { hash: *hash, header, })) - .with_stream_from_buffer(data) + .with_stream_from_buffer(data); + + if let Some(order_tag) = order_tag { + resp.with_order_tag(order_tag) + } else { + resp + } } /// Read block from disk, verifying it's integrity @@ -841,7 +864,7 @@ impl BlockManager { hash ); - let block_data = self.rpc_get_raw_block(hash).await?; + let block_data = self.rpc_get_raw_block(hash, None).await?; self.metrics.resync_recv_counter.add(1); @@ -861,7 +884,7 @@ impl StreamingEndpointHandler for BlockManager { .await .map(|_| BlockRpc::Ok), ), - BlockRpc::GetBlock(h) => self.handle_get_block(h).await, + BlockRpc::GetBlock(h, order_tag) => self.handle_get_block(h, *order_tag).await, BlockRpc::NeedBlockQuery(h) => { Resp::new(self.need_block(h).await.map(BlockRpc::NeedBlockReply)) } diff --git a/src/rpc/rpc_helper.rs b/src/rpc/rpc_helper.rs index 216fffd4..6c79c502 100644 --- a/src/rpc/rpc_helper.rs +++ b/src/rpc/rpc_helper.rs @@ -18,7 +18,7 @@ use opentelemetry::{ pub use netapp::endpoint::{Endpoint, EndpointHandler, StreamingEndpointHandler}; use netapp::message::IntoReq; pub use netapp::message::{ - Message as Rpc, Req, RequestPriority, Resp, PRIO_BACKGROUND, PRIO_HIGH, PRIO_NORMAL, + Message as Rpc, OrderTag, Req, RequestPriority, Resp, PRIO_BACKGROUND, PRIO_HIGH, PRIO_NORMAL, }; use netapp::peering::fullmesh::FullMeshPeeringStrategy; pub use netapp::{self, NetApp, NodeID}; From f3bf34b6a18c547c5fb29346787648048c093d52 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Thu, 1 Sep 2022 14:23:54 +0200 Subject: [PATCH 072/149] update netapp: straming + fix-ping --- Cargo.lock | 2 +- Cargo.nix | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bcb8f215..4c31d697 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2176,7 +2176,7 @@ dependencies = [ [[package]] name = "netapp" version = "0.5.0" -source = "git+https://git.deuxfleurs.fr/lx/netapp?branch=stream-body#4a59b73d7bfd0f136f654e874afb5d2a9bf4df2e" +source = "git+https://git.deuxfleurs.fr/lx/netapp?branch=stream-body#22d96929d5416750e1f5889ee6cc16b382293104" dependencies = [ "arc-swap", "async-trait", diff --git a/Cargo.nix b/Cargo.nix index 79f6ca7e..b42e1f95 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -791,7 +791,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -2852,7 +2852,7 @@ in [ "os-poll" ] ]; dependencies = { - ${ if hostPlatform.parsed.kernel.name == "wasi" || hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "wasi" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if hostPlatform.isWindows then "miow" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".miow."0.3.7" { inherit profileName; }; ${ if hostPlatform.isWindows then "ntapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ntapi."0.3.7" { inherit profileName; }; @@ -2965,13 +2965,12 @@ in url = https://git.deuxfleurs.fr/lx/netapp; name = "netapp"; version = "0.5.0"; - rev = "3fd30c6e280fba41377c8b563352d756e8bc1caf"; + rev = "22d96929d5416750e1f5889ee6cc16b382293104"; ref = "stream-body";}; features = builtins.concatLists [ [ "default" ] [ "opentelemetry" ] [ "opentelemetry-contrib" ] - [ "rand" ] [ "telemetry" ] ]; dependencies = { @@ -4055,7 +4054,7 @@ in ]; dependencies = { ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" || hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "i686" || hostPlatform.parsed.cpu.name == "x86_64" || (hostPlatform.parsed.cpu.name == "aarch64" || hostPlatform.parsed.cpu.name == "armv6l" || hostPlatform.parsed.cpu.name == "armv7l") && (hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "fuchsia" || hostPlatform.parsed.kernel.name == "linux") then "spin" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".spin."0.5.2" { inherit profileName; }; untrusted = rustPackages."registry+https://github.com/rust-lang/crates.io-index".untrusted."0.7.1" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "wasm32" && hostPlatform.parsed.vendor.name == "unknown" && hostPlatform.parsed.kernel.name == "unknown" && hostPlatform.parsed.abi.name == "" then "web_sys" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.56" { inherit profileName; }; @@ -5774,11 +5773,11 @@ in [ "default" ] ]; dependencies = { - ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-uwp-windows-gnu" || hostPlatform.config == "i686-pc-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); From df094bd8075332bb765b8b44c9b19cf2485e9ca8 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Thu, 1 Sep 2022 16:30:44 +0200 Subject: [PATCH 073/149] Less strict timeouts --- Cargo.lock | 2 +- src/block/manager.rs | 8 ++++++-- src/rpc/rpc_helper.rs | 2 +- src/rpc/system.rs | 6 +++--- src/table/gc.rs | 3 ++- src/table/sync.rs | 3 ++- src/table/table.rs | 2 +- 7 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c31d697..632c2131 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2176,7 +2176,7 @@ dependencies = [ [[package]] name = "netapp" version = "0.5.0" -source = "git+https://git.deuxfleurs.fr/lx/netapp?branch=stream-body#22d96929d5416750e1f5889ee6cc16b382293104" +source = "git+https://git.deuxfleurs.fr/lx/netapp?branch=stream-body#f6ad1d0fab340e77fbfcb3488a98c342d334838e" dependencies = [ "arc-swap", "async-trait", diff --git a/src/block/manager.rs b/src/block/manager.rs index b9f6fc0f..00438648 100644 --- a/src/block/manager.rs +++ b/src/block/manager.rs @@ -48,10 +48,14 @@ use crate::repair::*; pub const INLINE_THRESHOLD: usize = 3072; // Timeout for RPCs that read and write blocks to remote nodes -const BLOCK_RW_TIMEOUT: Duration = Duration::from_secs(30); +const BLOCK_RW_TIMEOUT: Duration = Duration::from_secs(60); // Timeout for RPCs that ask other nodes whether they need a copy // of a given block before we delete it locally -const NEED_BLOCK_QUERY_TIMEOUT: Duration = Duration::from_secs(5); +// The timeout here is relatively low because we don't want to block +// the entire resync loop when some nodes are not responding. +// Nothing will be deleted if the nodes don't answer the queries, +// we will just retry later. +const NEED_BLOCK_QUERY_TIMEOUT: Duration = Duration::from_secs(15); // The delay between the time where a resync operation fails // and the time when it is retried, with exponential backoff diff --git a/src/rpc/rpc_helper.rs b/src/rpc/rpc_helper.rs index 6c79c502..e9575261 100644 --- a/src/rpc/rpc_helper.rs +++ b/src/rpc/rpc_helper.rs @@ -31,7 +31,7 @@ use garage_util::metrics::RecordDuration; use crate::metrics::RpcMetrics; use crate::ring::Ring; -const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30); // Don't allow more than 100 concurrent outgoing RPCs. const MAX_CONCURRENT_REQUESTS: usize = 100; diff --git a/src/rpc/system.rs b/src/rpc/system.rs index 5858660e..d7ef2140 100644 --- a/src/rpc/system.rs +++ b/src/rpc/system.rs @@ -38,7 +38,7 @@ use crate::rpc_helper::*; const DISCOVERY_INTERVAL: Duration = Duration::from_secs(60); const STATUS_EXCHANGE_INTERVAL: Duration = Duration::from_secs(10); -const PING_TIMEOUT: Duration = Duration::from_secs(2); +const SYSTEM_RPC_TIMEOUT: Duration = Duration::from_secs(15); /// Version tag used for version check upon Netapp connection pub const GARAGE_VERSION_TAG: u64 = 0x6761726167650007; // garage 0x0007 @@ -561,7 +561,7 @@ impl System { .broadcast( &self.system_endpoint, SystemRpc::AdvertiseStatus(local_status), - RequestStrategy::with_priority(PRIO_HIGH).with_timeout(PING_TIMEOUT), + RequestStrategy::with_priority(PRIO_HIGH).with_timeout(SYSTEM_RPC_TIMEOUT), ) .await; @@ -685,7 +685,7 @@ impl System { &self.system_endpoint, peer, SystemRpc::PullClusterLayout, - RequestStrategy::with_priority(PRIO_HIGH).with_timeout(PING_TIMEOUT), + RequestStrategy::with_priority(PRIO_HIGH).with_timeout(SYSTEM_RPC_TIMEOUT), ) .await; if let Ok(SystemRpc::AdvertiseClusterLayout(layout)) = resp { diff --git a/src/table/gc.rs b/src/table/gc.rs index 12218d97..6cae9701 100644 --- a/src/table/gc.rs +++ b/src/table/gc.rs @@ -25,7 +25,8 @@ use crate::replication::*; use crate::schema::*; const TABLE_GC_BATCH_SIZE: usize = 1024; -const TABLE_GC_RPC_TIMEOUT: Duration = Duration::from_secs(30); +// Same timeout as NEED_BLOCK_QUERY_TIMEOUT in block manager +const TABLE_GC_RPC_TIMEOUT: Duration = Duration::from_secs(15); // GC delay for table entries: 1 day (24 hours) // (the delay before the entry is added in the GC todo list diff --git a/src/table/sync.rs b/src/table/sync.rs index b3756a5e..62b88a58 100644 --- a/src/table/sync.rs +++ b/src/table/sync.rs @@ -24,7 +24,8 @@ use crate::merkle::*; use crate::replication::*; use crate::*; -const TABLE_SYNC_RPC_TIMEOUT: Duration = Duration::from_secs(30); +// Sync RPC can contain a lot of data, so have a 1min timeout +const TABLE_SYNC_RPC_TIMEOUT: Duration = Duration::from_secs(60); // Do anti-entropy every 10 minutes const ANTI_ENTROPY_INTERVAL: Duration = Duration::from_secs(10 * 60); diff --git a/src/table/table.rs b/src/table/table.rs index 3c211728..51f3837f 100644 --- a/src/table/table.rs +++ b/src/table/table.rs @@ -31,7 +31,7 @@ use crate::schema::*; use crate::sync::*; use crate::util::*; -pub const TABLE_RPC_TIMEOUT: Duration = Duration::from_secs(10); +pub const TABLE_RPC_TIMEOUT: Duration = Duration::from_secs(30); pub struct Table { pub system: Arc, From e648bf7b69a487ca3f43d4ba607bfc2e354a5832 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Thu, 1 Sep 2022 16:31:04 +0200 Subject: [PATCH 074/149] update cargo.nix --- Cargo.nix | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.nix b/Cargo.nix index b42e1f95..9f477491 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -791,7 +791,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.config == "aarch64-linux-android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -2965,7 +2965,7 @@ in url = https://git.deuxfleurs.fr/lx/netapp; name = "netapp"; version = "0.5.0"; - rev = "22d96929d5416750e1f5889ee6cc16b382293104"; + rev = "f6ad1d0fab340e77fbfcb3488a98c342d334838e"; ref = "stream-body";}; features = builtins.concatLists [ [ "default" ] @@ -5773,11 +5773,11 @@ in [ "default" ] ]; dependencies = { - ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-uwp-windows-gnu" || hostPlatform.config == "i686-pc-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); From 99b532b85bf35b5acf621c229fb991825f3d994c Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Thu, 1 Sep 2022 16:35:43 +0200 Subject: [PATCH 075/149] Apply PRIO_SECONDARY to block data transfers --- src/block/manager.rs | 6 +++--- src/rpc/rpc_helper.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/block/manager.rs b/src/block/manager.rs index 00438648..a9def3b0 100644 --- a/src/block/manager.rs +++ b/src/block/manager.rs @@ -198,7 +198,7 @@ impl BlockManager { let rpc = self.endpoint.call_streaming( &node_id, BlockRpc::GetBlock(*hash, order_tag), - PRIO_NORMAL, + PRIO_NORMAL | PRIO_SECONDARY, ); tokio::select! { res = rpc => { @@ -245,7 +245,7 @@ impl BlockManager { let rpc = self.endpoint.call_streaming( &node_id, BlockRpc::GetBlock(*hash, order_tag), - PRIO_NORMAL, + PRIO_NORMAL | PRIO_SECONDARY, ); tokio::select! { res = rpc => { @@ -336,7 +336,7 @@ impl BlockManager { &self.endpoint, &who[..], put_block_rpc, - RequestStrategy::with_priority(PRIO_NORMAL) + RequestStrategy::with_priority(PRIO_NORMAL | PRIO_SECONDARY) .with_quorum(self.replication.write_quorum()) .with_timeout(BLOCK_RW_TIMEOUT), ) diff --git a/src/rpc/rpc_helper.rs b/src/rpc/rpc_helper.rs index e9575261..aa204c5e 100644 --- a/src/rpc/rpc_helper.rs +++ b/src/rpc/rpc_helper.rs @@ -18,7 +18,7 @@ use opentelemetry::{ pub use netapp::endpoint::{Endpoint, EndpointHandler, StreamingEndpointHandler}; use netapp::message::IntoReq; pub use netapp::message::{ - Message as Rpc, OrderTag, Req, RequestPriority, Resp, PRIO_BACKGROUND, PRIO_HIGH, PRIO_NORMAL, + Message as Rpc, OrderTag, Req, RequestPriority, Resp, PRIO_BACKGROUND, PRIO_HIGH, PRIO_NORMAL, PRIO_SECONDARY }; use netapp::peering::fullmesh::FullMeshPeeringStrategy; pub use netapp::{self, NetApp, NodeID}; From 1ef87ac4cb676113e86fc16a9eb27546d9a737bd Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 2 Sep 2022 13:38:29 +0200 Subject: [PATCH 076/149] cargo fmt --- src/rpc/rpc_helper.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rpc/rpc_helper.rs b/src/rpc/rpc_helper.rs index aa204c5e..19abb4c5 100644 --- a/src/rpc/rpc_helper.rs +++ b/src/rpc/rpc_helper.rs @@ -18,7 +18,8 @@ use opentelemetry::{ pub use netapp::endpoint::{Endpoint, EndpointHandler, StreamingEndpointHandler}; use netapp::message::IntoReq; pub use netapp::message::{ - Message as Rpc, OrderTag, Req, RequestPriority, Resp, PRIO_BACKGROUND, PRIO_HIGH, PRIO_NORMAL, PRIO_SECONDARY + Message as Rpc, OrderTag, Req, RequestPriority, Resp, PRIO_BACKGROUND, PRIO_HIGH, PRIO_NORMAL, + PRIO_SECONDARY, }; use netapp::peering::fullmesh::FullMeshPeeringStrategy; pub use netapp::{self, NetApp, NodeID}; From 13b5f28c7e8dec12b1db61735931b3830a3c893f Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 2 Sep 2022 13:46:42 +0200 Subject: [PATCH 077/149] Make use of BytesBuf from new Netapp --- src/api/s3/put.rs | 43 ++++++++++--------------------------------- 1 file changed, 10 insertions(+), 33 deletions(-) diff --git a/src/api/s3/put.rs b/src/api/s3/put.rs index dc0530df..97b8e4e3 100644 --- a/src/api/s3/put.rs +++ b/src/api/s3/put.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::sync::Arc; use futures::prelude::*; @@ -13,6 +13,7 @@ use opentelemetry::{ Context, }; +use garage_rpc::netapp::bytes_buf::BytesBuf; use garage_table::*; use garage_util::async_hash::*; use garage_util::data::*; @@ -108,7 +109,7 @@ pub(crate) async fn save_stream> + Unpin>( size, etag: data_md5sum_hex.clone(), }, - first_block, + first_block.to_vec(), )), }; @@ -136,7 +137,6 @@ pub(crate) async fn save_stream> + Unpin>( garage.version_table.insert(&version).await?; // Transfer data and verify checksum - let first_block = Bytes::from(first_block); let first_block_hash = async_blake2sum(first_block.clone()).await; let tx_result = (|| async { @@ -318,7 +318,6 @@ async fn read_and_put_blocks> + Unpin>( chunker.next(), )?; if let Some(block) = next_block { - let block = Bytes::from(block); let (_, _, block_hash) = futures::future::join3( md5hasher.update(block.clone()), sha256hasher.update(block.clone()), @@ -387,8 +386,7 @@ struct StreamChunker>> { stream: S, read_all: bool, block_size: usize, - buf: VecDeque, - buf_len: usize, + buf: BytesBuf, } impl> + Unpin> StreamChunker { @@ -397,45 +395,25 @@ impl> + Unpin> StreamChunker { stream, read_all: false, block_size, - buf: VecDeque::with_capacity(8), - buf_len: 0, + buf: BytesBuf::new(), } } - async fn next(&mut self) -> Result>, Error> { - while !self.read_all && self.buf_len < self.block_size { + async fn next(&mut self) -> Result, Error> { + while !self.read_all && self.buf.len() < self.block_size { if let Some(block) = self.stream.next().await { let bytes = block?; trace!("Body next: {} bytes", bytes.len()); - self.buf_len += bytes.len(); - self.buf.push_back(bytes); + self.buf.extend(bytes); } else { self.read_all = true; } } - if self.buf_len == 0 { + if self.buf.is_empty() { Ok(None) } else { - let mut slices = Vec::with_capacity(self.buf.len()); - let mut taken = 0; - while self.buf_len > 0 && taken < self.block_size { - let front = self.buf.pop_front().unwrap(); - if taken + front.len() <= self.block_size { - taken += front.len(); - self.buf_len -= front.len(); - slices.push(front); - } else { - let front_take = self.block_size - taken; - slices.push(front.slice(..front_take)); - self.buf.push_front(front.slice(front_take..)); - self.buf_len -= front_take; - break; - } - } - Ok(Some( - slices.iter().map(|x| &x[..]).collect::>().concat(), - )) + Ok(Some(self.buf.take_max(self.block_size))) } } } @@ -545,7 +523,6 @@ pub async fn handle_put_part( // Copy block to store let version = Version::new(version_uuid, bucket_id, key, false); - let first_block = Bytes::from(first_block); let first_block_hash = async_blake2sum(first_block.clone()).await; let (_, data_md5sum, data_sha256sum) = read_and_put_blocks( From 6226f5ceca7828d096890c3dbc5b9fbc3f7c4b14 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 2 Sep 2022 14:33:12 +0200 Subject: [PATCH 078/149] Update to netapp 0.4.5 - fixed ping --- Cargo.lock | 36 +++++++------------- Cargo.nix | 82 +++++++++++++++++----------------------------- Makefile | 2 +- src/rpc/Cargo.toml | 2 +- 4 files changed, 44 insertions(+), 78 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96a8cc61..2e1c4992 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1009,7 +1009,7 @@ dependencies = [ "http", "hyper", "kuska-sodiumoxide", - "netapp 0.4.4", + "netapp 0.4.5", "opentelemetry", "opentelemetry-otlp", "opentelemetry-prometheus", @@ -1156,7 +1156,7 @@ dependencies = [ "garage_table 0.7.0", "garage_util 0.7.0", "hex", - "netapp 0.4.4", + "netapp 0.4.5", "opentelemetry", "rand 0.8.5", "rmp-serde 0.15.5", @@ -1210,7 +1210,7 @@ dependencies = [ "k8s-openapi", "kube", "kuska-sodiumoxide", - "netapp 0.4.4", + "netapp 0.4.5", "openssl", "opentelemetry", "pnet_datalink", @@ -1310,7 +1310,7 @@ dependencies = [ "hex", "http", "hyper", - "netapp 0.4.4", + "netapp 0.4.5", "opentelemetry", "rand 0.8.5", "rmp-serde 0.15.5", @@ -2159,15 +2159,15 @@ dependencies = [ [[package]] name = "netapp" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6419a4b836774192e13fedb05c0e5f414ee8df9ca0c467456a0bacde06c29ee" +checksum = "38e390e559aff9e3865c7cd326c691cbf25c580a96f259d2debc35bd4e14018e" dependencies = [ "arc-swap", "async-trait", - "bytes 0.6.0", + "bytes 1.1.0", "cfg-if 1.0.0", - "err-derive 0.2.4", + "err-derive 0.3.1", "futures", "hex", "kuska-handshake", @@ -2175,12 +2175,12 @@ dependencies = [ "log", "opentelemetry", "opentelemetry-contrib", - "rand 0.5.6", - "rmp-serde 0.14.4", + "rand 0.8.5", + "rmp-serde 0.15.5", "serde", "tokio", "tokio-stream", - "tokio-util 0.6.9", + "tokio-util 0.7.0", ] [[package]] @@ -2736,19 +2736,6 @@ dependencies = [ "proc-macro2", ] -[[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.6.5" @@ -3730,6 +3717,7 @@ checksum = "64910e1b9c1901aaf5375561e35b9c057d95ff41a44ede043a03e09279eabaf1" dependencies = [ "bytes 1.1.0", "futures-core", + "futures-io", "futures-sink", "log", "pin-project-lite", diff --git a/Cargo.nix b/Cargo.nix index 97afbd64..33cab051 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -769,7 +769,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -1427,7 +1427,7 @@ in garage_web = rustPackages."unknown".garage_web."0.7.0" { inherit profileName; }; hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; sodiumoxide = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; - netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; + netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.5" { inherit profileName; }; opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; opentelemetry_otlp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-otlp."0.10.0" { inherit profileName; }; opentelemetry_prometheus = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-prometheus."0.10.0" { inherit profileName; }; @@ -1609,7 +1609,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_table" else null } = rustPackages."unknown".garage_table."0.7.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; @@ -1674,7 +1674,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "k8s_openapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".k8s-openapi."0.13.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "kube" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kube."0.62.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "sodiumoxide" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "openssl" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".openssl."0.10.38" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "pnet_datalink" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pnet_datalink."0.28.0" { inherit profileName; }; @@ -1785,7 +1785,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "http" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "hyper" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; @@ -2933,11 +2933,11 @@ in }; }); - "registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" = overridableMkRustCrate (profileName: rec { + "registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.5" = overridableMkRustCrate (profileName: rec { name = "netapp"; - version = "0.4.4"; + version = "0.4.5"; registry = "registry+https://github.com/rust-lang/crates.io-index"; - src = fetchCratesIo { inherit name version; sha256 = "c6419a4b836774192e13fedb05c0e5f414ee8df9ca0c467456a0bacde06c29ee"; }; + src = fetchCratesIo { inherit name version; sha256 = "38e390e559aff9e3865c7cd326c691cbf25c580a96f259d2debc35bd4e14018e"; }; features = builtins.concatLists [ (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "opentelemetry") @@ -2948,9 +2948,9 @@ in dependencies = { ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "arc_swap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "async_trait" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."0.6.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "cfg_if" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "err_derive" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.2.4" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "err_derive" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "kuska_handshake" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-handshake."0.2.0" { inherit profileName; }; @@ -2958,12 +2958,12 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "log" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "opentelemetry_contrib" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-contrib."0.9.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.5.6" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.14.4" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio_stream" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.8" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.6.9" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.0" { inherit profileName; }; }; }); @@ -3689,29 +3689,6 @@ in }; }); - "registry+https://github.com/rust-lang/crates.io-index".rand."0.5.6" = overridableMkRustCrate (profileName: rec { - name = "rand"; - version = "0.5.6"; - registry = "registry+https://github.com/rust-lang/crates.io-index"; - src = fetchCratesIo { inherit name version; sha256 = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9"; }; - features = builtins.concatLists [ - [ "alloc" ] - [ "cloudabi" ] - [ "default" ] - [ "fuchsia-cprng" ] - [ "libc" ] - [ "std" ] - [ "winapi" ] - ]; - dependencies = { - ${ if hostPlatform.parsed.kernel.name == "cloudabi" then "cloudabi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cloudabi."0.0.3" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "fuchsia" then "fuchsia_cprng" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".fuchsia-cprng."0.1.1" { inherit profileName; }; - ${ if hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - rand_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_core."0.3.1" { inherit profileName; }; - ${ if hostPlatform.isWindows then "winapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".winapi."0.3.9" { inherit profileName; }; - }; - }); - "registry+https://github.com/rust-lang/crates.io-index".rand."0.6.5" = overridableMkRustCrate (profileName: rec { name = "rand"; version = "0.6.5"; @@ -3794,10 +3771,6 @@ in version = "0.3.1"; registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"; }; - features = builtins.concatLists [ - [ "alloc" ] - [ "std" ] - ]; dependencies = { rand_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_core."0.4.2" { inherit profileName; }; }; @@ -4647,7 +4620,7 @@ in ]; dependencies = { bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot_core."0.8.5" { inherit profileName; }; static_init_macro = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init_macro."1.0.2" { profileName = "__noProfile"; }; @@ -5025,22 +4998,22 @@ in src = fetchCratesIo { inherit name version; sha256 = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0"; }; features = builtins.concatLists [ (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "codec") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "compat") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web") "compat") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "futures-io") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web") "futures-io") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "io") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "slab") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "time") ]; dependencies = { - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_io" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-io."0.3.21" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_sink" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-sink."0.3.21" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "log" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "pin_project_lite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "futures_io" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-io."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_sink" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-sink."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "log" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "pin_project_lite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "slab" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".slab."0.4.5" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; }; }); @@ -5049,9 +5022,14 @@ in version = "0.7.0"; registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "64910e1b9c1901aaf5375561e35b9c057d95ff41a44ede043a03e09279eabaf1"; }; + features = builtins.concatLists [ + [ "compat" ] + [ "futures-io" ] + ]; dependencies = { bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; futures_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; + futures_io = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-io."0.3.21" { inherit profileName; }; futures_sink = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-sink."0.3.21" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; @@ -5744,8 +5722,8 @@ in [ "default" ] ]; dependencies = { - ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-uwp-windows-gnu" || hostPlatform.config == "i686-pc-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; diff --git a/Makefile b/Makefile index 1f0f3644..23e10f78 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: doc all release shell run1 run2 run3 all: - clear; cargo build --all-features + clear; cargo build release: nix-build --arg release true diff --git a/src/rpc/Cargo.toml b/src/rpc/Cargo.toml index 80a1975c..5757fe8d 100644 --- a/src/rpc/Cargo.toml +++ b/src/rpc/Cargo.toml @@ -47,7 +47,7 @@ opentelemetry = "0.17" #netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" } #netapp = { version = "0.4", path = "../../../netapp", features = ["telemetry"] } -netapp = { version = "0.4.4", features = ["telemetry"] } +netapp = { version = "0.4.5", features = ["telemetry"] } hyper = { version = "0.14", features = ["client", "http1", "runtime", "tcp"] } From 943d76c583f5740b1d922275a673233a27fe1693 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 2 Sep 2022 15:34:21 +0200 Subject: [PATCH 079/149] Ability to dynamically set resync tranquility --- src/block/manager.rs | 62 ++++++++++++++++++++++++++++++++------- src/block/repair.rs | 26 +++++++++++++--- src/garage/admin.rs | 19 ++++++++++++ src/garage/cli/structs.rs | 16 ++++++++++ src/model/garage.rs | 1 - src/util/config.rs | 7 ----- 6 files changed, 108 insertions(+), 23 deletions(-) diff --git a/src/block/manager.rs b/src/block/manager.rs index 017ba9da..ef48107f 100644 --- a/src/block/manager.rs +++ b/src/block/manager.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; -use arc_swap::ArcSwapOption; +use arc_swap::{ArcSwap, ArcSwapOption}; use async_trait::async_trait; use serde::{Deserialize, Serialize}; @@ -25,6 +25,7 @@ use garage_util::background::*; use garage_util::data::*; use garage_util::error::*; use garage_util::metrics::RecordDuration; +use garage_util::persister::Persister; use garage_util::time::*; use garage_util::tranquilizer::Tranquilizer; @@ -55,6 +56,10 @@ const RESYNC_RETRY_DELAY: Duration = Duration::from_secs(60); // The maximum retry delay is 60 seconds * 2^6 = 60 seconds << 6 = 64 minutes (~1 hour) const RESYNC_RETRY_DELAY_MAX_BACKOFF_POWER: u64 = 6; +// Resync tranquility is initially set to 2, but can be changed in the CLI +// and the updated version is persisted over Garage restarts +const INITIAL_RESYNC_TRANQUILITY: u32 = 2; + // The delay between the moment when the reference counter // drops to zero, and the moment where we allow ourselves // to delete the block locally. @@ -90,7 +95,6 @@ pub struct BlockManager { pub data_dir: PathBuf, compression_level: Option, - background_tranquility: u32, mutation_lock: Mutex, @@ -100,6 +104,9 @@ pub struct BlockManager { resync_notify: Notify, resync_errors: CountedTree, + resync_persister: Persister, + resync_persisted: ArcSwap, + pub(crate) system: Arc, endpoint: Arc>, @@ -124,7 +131,6 @@ impl BlockManager { db: &db::Db, data_dir: PathBuf, compression_level: Option, - background_tranquility: u32, replication: TableShardedReplication, system: Arc, ) -> Arc { @@ -145,6 +151,14 @@ impl BlockManager { let resync_errors = CountedTree::new(resync_errors).expect("Could not count block_local_resync_errors"); + let resync_persister = Persister::new(&system.metadata_dir, "resync_cfg"); + let resync_persisted = match resync_persister.load() { + Ok(v) => v, + Err(_) => ResyncPersistedConfig { + tranquility: INITIAL_RESYNC_TRANQUILITY, + }, + }; + let endpoint = system .netapp .endpoint("garage_block/manager.rs/Rpc".to_string()); @@ -157,12 +171,13 @@ impl BlockManager { replication, data_dir, compression_level, - background_tranquility, mutation_lock: Mutex::new(manager_locked), rc, resync_queue, resync_notify: Notify::new(), resync_errors, + resync_persister, + resync_persisted: ArcSwap::new(Arc::new(resync_persisted)), system, endpoint, metrics, @@ -716,6 +731,23 @@ impl BlockManager { Ok(()) } + + async fn update_resync_persisted( + &self, + update: impl Fn(&mut ResyncPersistedConfig), + ) -> Result<(), Error> { + let mut cfg: ResyncPersistedConfig = *self.resync_persisted.load().as_ref(); + update(&mut cfg); + self.resync_persister.save_async(&cfg).await?; + self.resync_persisted.store(Arc::new(cfg)); + self.resync_notify.notify_one(); + Ok(()) + } + + pub async fn set_resync_tranquility(&self, tranquility: u32) -> Result<(), Error> { + self.update_resync_persisted(|cfg| cfg.tranquility = tranquility) + .await + } } #[async_trait] @@ -734,6 +766,11 @@ impl EndpointHandler for BlockManager { } } +#[derive(Serialize, Deserialize, Clone, Copy)] +struct ResyncPersistedConfig { + tranquility: u32, +} + struct ResyncWorker { manager: Arc, tranquilizer: Tranquilizer, @@ -758,19 +795,22 @@ impl Worker for ResyncWorker { fn info(&self) -> Option { let mut ret = vec![]; + ret.push(format!( + "tranquility = {}", + self.manager.resync_persisted.load().tranquility + )); + let qlen = self.manager.resync_queue_len().unwrap_or(0); - let elen = self.manager.resync_errors_len().unwrap_or(0); if qlen > 0 { ret.push(format!("{} blocks in queue", qlen)); } + + let elen = self.manager.resync_errors_len().unwrap_or(0); if elen > 0 { ret.push(format!("{} blocks in error state", elen)); } - if !ret.is_empty() { - Some(ret.join(", ")) - } else { - None - } + + Some(ret.join(", ")) } async fn work(&mut self, _must_exit: &mut watch::Receiver) -> Result { @@ -778,7 +818,7 @@ impl Worker for ResyncWorker { match self.manager.resync_iter().await { Ok(ResyncIterResult::BusyDidSomething) => Ok(self .tranquilizer - .tranquilize_worker(self.manager.background_tranquility)), + .tranquilize_worker(self.manager.resync_persisted.load().tranquility)), Ok(ResyncIterResult::BusyDidNothing) => Ok(WorkerState::Busy), Ok(ResyncIterResult::IdleFor(delay)) => { self.next_delay = delay; diff --git a/src/block/repair.rs b/src/block/repair.rs index 07ff6772..18e1de95 100644 --- a/src/block/repair.rs +++ b/src/block/repair.rs @@ -19,7 +19,17 @@ use garage_util::tranquilizer::Tranquilizer; use crate::manager::*; -const SCRUB_INTERVAL: Duration = Duration::from_secs(3600 * 24 * 30); // full scrub every 30 days +// Full scrub every 30 days +const SCRUB_INTERVAL: Duration = Duration::from_secs(3600 * 24 * 30); +// Scrub tranquility is initially set to 4, but can be changed in the CLI +// and the updated version is persisted over Garage restarts +const INITIAL_SCRUB_TRANQUILITY: u32 = 4; + +// ---- ---- ---- +// FIRST KIND OF REPAIR: FINDING MISSING BLOCKS/USELESS BLOCKS +// This is a one-shot repair operation that can be launched, +// checks everything, and then exits. +// ---- ---- ---- pub struct RepairWorker { manager: Arc, @@ -128,7 +138,13 @@ impl Worker for RepairWorker { } } -// ---- +// ---- ---- ---- +// SECOND KIND OF REPAIR: SCRUBBING THE DATASTORE +// This is significantly more complex than the process above, +// as it is a continuously-running task that triggers automatically +// every SCRUB_INTERVAL, but can also be triggered manually +// and whose parameter (esp. speed) can be controlled at runtime. +// ---- ---- ---- pub struct ScrubWorker { manager: Arc, @@ -176,7 +192,7 @@ impl ScrubWorker { Ok(v) => v, Err(_) => ScrubWorkerPersisted { time_last_complete_scrub: 0, - tranquility: 4, + tranquility: INITIAL_SCRUB_TRANQUILITY, corruptions_detected: 0, }, }; @@ -343,7 +359,9 @@ impl Worker for ScrubWorker { } } -// ---- +// ---- ---- ---- +// UTILITY FOR ENUMERATING THE BLOCK STORE +// ---- ---- ---- struct BlockStoreIterator { path: Vec, diff --git a/src/garage/admin.rs b/src/garage/admin.rs index 71ee608c..1d80889c 100644 --- a/src/garage/admin.rs +++ b/src/garage/admin.rs @@ -15,6 +15,8 @@ use garage_table::*; use garage_rpc::*; +use garage_block::repair::ScrubWorkerCommand; + use garage_model::bucket_alias_table::*; use garage_model::bucket_table::*; use garage_model::garage::Garage; @@ -836,6 +838,23 @@ impl AdminRpcHandler { let workers = self.garage.background.get_worker_info(); Ok(AdminRpc::WorkerList(workers, opt)) } + WorkerCmd::Set { opt } => match opt { + WorkerSetCmd::ScrubTranquility { tranquility } => { + let scrub_command = ScrubWorkerCommand::SetTranquility(tranquility); + self.garage + .block_manager + .send_scrub_command(scrub_command) + .await; + Ok(AdminRpc::Ok("Scrub tranquility updated".into())) + } + WorkerSetCmd::ResyncTranquility { tranquility } => { + self.garage + .block_manager + .set_resync_tranquility(tranquility) + .await?; + Ok(AdminRpc::Ok("Resync tranquility updated".into())) + } + }, } } } diff --git a/src/garage/cli/structs.rs b/src/garage/cli/structs.rs index 9274f80f..1fba934f 100644 --- a/src/garage/cli/structs.rs +++ b/src/garage/cli/structs.rs @@ -501,6 +501,12 @@ pub enum WorkerCmd { #[structopt(flatten)] opt: WorkerListOpt, }, + /// Set worker parameter + #[structopt(name = "set", version = version::garage())] + Set { + #[structopt(subcommand)] + opt: WorkerSetCmd, + }, } #[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone, Copy)] @@ -512,3 +518,13 @@ pub struct WorkerListOpt { #[structopt(short = "e", long = "errors")] pub errors: bool, } + +#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)] +pub enum WorkerSetCmd { + /// Set tranquility of scrub operations + #[structopt(name = "scrub-tranquility", version = version::garage())] + ScrubTranquility { tranquility: u32 }, + /// Set tranquility of resync operations + #[structopt(name = "resync-tranquility", version = version::garage())] + ResyncTranquility { tranquility: u32 }, +} diff --git a/src/model/garage.rs b/src/model/garage.rs index 15769a17..4dd24582 100644 --- a/src/model/garage.rs +++ b/src/model/garage.rs @@ -159,7 +159,6 @@ impl Garage { &db, config.data_dir.clone(), config.compression_level, - config.block_manager_background_tranquility, data_rep_param, system.clone(), ); diff --git a/src/util/config.rs b/src/util/config.rs index e8ef4fdd..a2bb8fb3 100644 --- a/src/util/config.rs +++ b/src/util/config.rs @@ -23,10 +23,6 @@ pub struct Config { #[serde(default = "default_block_size")] pub block_size: usize, - /// Size of data blocks to save to disk - #[serde(default = "default_block_manager_background_tranquility")] - pub block_manager_background_tranquility: u32, - /// Replication mode. Supported values: /// - none, 1 -> no replication /// - 2 -> 2-way replication @@ -147,9 +143,6 @@ fn default_sled_flush_every_ms() -> u64 { fn default_block_size() -> usize { 1048576 } -fn default_block_manager_background_tranquility() -> u32 { - 2 -} /// Read and parse configuration pub fn read_config(config_file: PathBuf) -> Result { From 47be652a1fe08a8e6dab6aa2c4a41d8eb119f392 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 2 Sep 2022 16:47:15 +0200 Subject: [PATCH 080/149] block manager: refactor: split resync into separate file --- src/block/lib.rs | 1 + src/block/manager.rs | 598 ++++--------------------------------------- src/block/repair.rs | 8 +- src/block/resync.rs | 536 ++++++++++++++++++++++++++++++++++++++ src/garage/admin.rs | 7 +- 5 files changed, 595 insertions(+), 555 deletions(-) create mode 100644 src/block/resync.rs diff --git a/src/block/lib.rs b/src/block/lib.rs index ebdb95d8..d2814f77 100644 --- a/src/block/lib.rs +++ b/src/block/lib.rs @@ -3,6 +3,7 @@ extern crate tracing; pub mod manager; pub mod repair; +pub mod resync; mod block; mod metrics; diff --git a/src/block/manager.rs b/src/block/manager.rs index ef48107f..efb5349c 100644 --- a/src/block/manager.rs +++ b/src/block/manager.rs @@ -1,33 +1,19 @@ -use std::convert::TryInto; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; -use arc_swap::{ArcSwap, ArcSwapOption}; use async_trait::async_trait; use serde::{Deserialize, Serialize}; -use futures::future::*; use tokio::fs; use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::select; -use tokio::sync::{mpsc, watch, Mutex, Notify}; - -use opentelemetry::{ - trace::{FutureExt as OtelFutureExt, TraceContextExt, Tracer}, - Context, KeyValue, -}; +use tokio::sync::{mpsc, Mutex}; use garage_db as db; -use garage_db::counted_tree_hack::CountedTree; -use garage_util::background::*; use garage_util::data::*; use garage_util::error::*; use garage_util::metrics::RecordDuration; -use garage_util::persister::Persister; -use garage_util::time::*; -use garage_util::tranquilizer::Tranquilizer; use garage_rpc::system::System; use garage_rpc::*; @@ -38,27 +24,13 @@ use crate::block::*; use crate::metrics::*; use crate::rc::*; use crate::repair::*; +use crate::resync::*; /// Size under which data will be stored inlined in database instead of as files pub const INLINE_THRESHOLD: usize = 3072; // Timeout for RPCs that read and write blocks to remote nodes -const BLOCK_RW_TIMEOUT: Duration = Duration::from_secs(30); -// Timeout for RPCs that ask other nodes whether they need a copy -// of a given block before we delete it locally -const NEED_BLOCK_QUERY_TIMEOUT: Duration = Duration::from_secs(5); - -// The delay between the time where a resync operation fails -// and the time when it is retried, with exponential backoff -// (multiplied by 2, 4, 8, 16, etc. for every consecutive failure). -const RESYNC_RETRY_DELAY: Duration = Duration::from_secs(60); -// The minimum retry delay is 60 seconds = 1 minute -// The maximum retry delay is 60 seconds * 2^6 = 60 seconds << 6 = 64 minutes (~1 hour) -const RESYNC_RETRY_DELAY_MAX_BACKOFF_POWER: u64 = 6; - -// Resync tranquility is initially set to 2, but can be changed in the CLI -// and the updated version is persisted over Garage restarts -const INITIAL_RESYNC_TRANQUILITY: u32 = 2; +pub(crate) const BLOCK_RW_TIMEOUT: Duration = Duration::from_secs(30); // The delay between the moment when the reference counter // drops to zero, and the moment where we allow ourselves @@ -96,35 +68,23 @@ pub struct BlockManager { compression_level: Option, - mutation_lock: Mutex, + pub(crate) mutation_lock: Mutex, pub(crate) rc: BlockRc, - - resync_queue: CountedTree, - resync_notify: Notify, - resync_errors: CountedTree, - - resync_persister: Persister, - resync_persisted: ArcSwap, + pub resync: BlockResyncManager, pub(crate) system: Arc, - endpoint: Arc>, + pub(crate) endpoint: Arc>, - metrics: BlockManagerMetrics, + pub(crate) metrics: BlockManagerMetrics, - tx_scrub_command: ArcSwapOption>, + tx_scrub_command: mpsc::Sender, } // This custom struct contains functions that must only be ran // when the lock is held. We ensure that it is the case by storing // it INSIDE a Mutex. -struct BlockManagerLocked(); - -enum ResyncIterResult { - BusyDidSomething, - BusyDidNothing, - IdleFor(Duration), -} +pub(crate) struct BlockManagerLocked(); impl BlockManager { pub fn new( @@ -139,25 +99,7 @@ impl BlockManager { .expect("Unable to open block_local_rc tree"); let rc = BlockRc::new(rc); - let resync_queue = db - .open_tree("block_local_resync_queue") - .expect("Unable to open block_local_resync_queue tree"); - let resync_queue = - CountedTree::new(resync_queue).expect("Could not count block_local_resync_queue"); - - let resync_errors = db - .open_tree("block_local_resync_errors") - .expect("Unable to open block_local_resync_errors tree"); - let resync_errors = - CountedTree::new(resync_errors).expect("Could not count block_local_resync_errors"); - - let resync_persister = Persister::new(&system.metadata_dir, "resync_cfg"); - let resync_persisted = match resync_persister.load() { - Ok(v) => v, - Err(_) => ResyncPersistedConfig { - tranquility: INITIAL_RESYNC_TRANQUILITY, - }, - }; + let resync = BlockResyncManager::new(db, &system); let endpoint = system .netapp @@ -165,7 +107,9 @@ impl BlockManager { let manager_locked = BlockManagerLocked(); - let metrics = BlockManagerMetrics::new(resync_queue.clone(), resync_errors.clone()); + let metrics = BlockManagerMetrics::new(resync.queue.clone(), resync.errors.clone()); + + let (scrub_tx, scrub_rx) = mpsc::channel(1); let block_manager = Arc::new(Self { replication, @@ -173,25 +117,31 @@ impl BlockManager { compression_level, mutation_lock: Mutex::new(manager_locked), rc, - resync_queue, - resync_notify: Notify::new(), - resync_errors, - resync_persister, - resync_persisted: ArcSwap::new(Arc::new(resync_persisted)), + resync, system, endpoint, metrics, - tx_scrub_command: ArcSwapOption::new(None), + tx_scrub_command: scrub_tx, }); block_manager.endpoint.set_handler(block_manager.clone()); - block_manager.clone().spawn_background_workers(); + // Spawn one resync worker + let background = block_manager.system.background.clone(); + let worker = ResyncWorker::new(block_manager.clone()); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(10)).await; + background.spawn_worker(worker); + }); + + // Spawn scrub worker + let scrub_worker = ScrubWorker::new(block_manager.clone(), scrub_rx); + block_manager.system.background.spawn_worker(scrub_worker); block_manager } /// Ask nodes that might have a (possibly compressed) block for it - async fn rpc_get_raw_block(&self, hash: &Hash) -> Result { + pub(crate) async fn rpc_get_raw_block(&self, hash: &Hash) -> Result { let who = self.replication.read_nodes(hash); let resps = self .system @@ -243,20 +193,6 @@ impl BlockManager { Ok(()) } - /// Get lenght of resync queue - pub fn resync_queue_len(&self) -> Result { - // This currently can't return an error because the CountedTree hack - // doesn't error on .len(), but this will change when we remove the hack - // (hopefully someday!) - Ok(self.resync_queue.len()) - } - - /// Get number of blocks that have an error - pub fn resync_errors_len(&self) -> Result { - // (see resync_queue_len comment) - Ok(self.resync_errors.len()) - } - /// Get number of items in the refcount table pub fn rc_len(&self) -> Result { Ok(self.rc.rc.len()?) @@ -264,13 +200,7 @@ impl BlockManager { /// Send command to start/stop/manager scrub worker pub async fn send_scrub_command(&self, cmd: ScrubWorkerCommand) { - let _ = self - .tx_scrub_command - .load() - .as_ref() - .unwrap() - .send(cmd) - .await; + let _ = self.tx_scrub_command.send(cmd).await; } //// ----- Managing the reference counter ---- @@ -291,7 +221,7 @@ impl BlockManager { // we will fecth it from someone. let this = self.clone(); tokio::spawn(async move { - if let Err(e) = this.put_to_resync(&hash, 2 * BLOCK_RW_TIMEOUT) { + if let Err(e) = this.resync.put_to_resync(&hash, 2 * BLOCK_RW_TIMEOUT) { error!("Block {:?} could not be put in resync queue: {}.", hash, e); } }); @@ -313,7 +243,9 @@ impl BlockManager { // after that delay has passed. let this = self.clone(); tokio::spawn(async move { - if let Err(e) = this.put_to_resync(&hash, BLOCK_GC_DELAY + Duration::from_secs(10)) + if let Err(e) = this + .resync + .put_to_resync(&hash, BLOCK_GC_DELAY + Duration::from_secs(10)) { error!("Block {:?} could not be put in resync queue: {}.", hash, e); } @@ -325,7 +257,11 @@ impl BlockManager { // ---- Reading and writing blocks locally ---- /// Write a block to disk - async fn write_block(&self, hash: &Hash, data: &DataBlock) -> Result { + pub(crate) async fn write_block( + &self, + hash: &Hash, + data: &DataBlock, + ) -> Result { let write_size = data.inner_buffer().len() as u64; let res = self @@ -361,7 +297,7 @@ impl BlockManager { Ok(c) => c, Err(e) => { // Not found but maybe we should have had it ?? - self.put_to_resync(hash, 2 * BLOCK_RW_TIMEOUT)?; + self.resync.put_to_resync(hash, 2 * BLOCK_RW_TIMEOUT)?; return Err(Into::into(e)); } }; @@ -388,7 +324,7 @@ impl BlockManager { .await .move_block_to_corrupted(hash, self) .await?; - self.put_to_resync(hash, Duration::from_millis(0))?; + self.resync.put_to_resync(hash, Duration::from_millis(0))?; return Err(Error::CorruptData(*hash)); } @@ -432,322 +368,6 @@ impl BlockManager { path.set_extension(""); fs::metadata(&path).await.map(|_| false).map_err(Into::into) } - - // ---- Resync loop ---- - - // This part manages a queue of blocks that need to be - // "resynchronized", i.e. that need to have a check that - // they are at present if we need them, or that they are - // deleted once the garbage collection delay has passed. - // - // Here are some explanations on how the resync queue works. - // There are two Sled trees that are used to have information - // about the status of blocks that need to be resynchronized: - // - // - resync_queue: a tree that is ordered first by a timestamp - // (in milliseconds since Unix epoch) that is the time at which - // the resync must be done, and second by block hash. - // The key in this tree is just: - // concat(timestamp (8 bytes), hash (32 bytes)) - // The value is the same 32-byte hash. - // - // - resync_errors: a tree that indicates for each block - // if the last resync resulted in an error, and if so, - // the following two informations (see the ErrorCounter struct): - // - how many consecutive resync errors for this block? - // - when was the last try? - // These two informations are used to implement an - // exponential backoff retry strategy. - // The key in this tree is the 32-byte hash of the block, - // and the value is the encoded ErrorCounter value. - // - // We need to have these two trees, because the resync queue - // is not just a queue of items to process, but a set of items - // that are waiting a specific delay until we can process them - // (the delay being necessary both internally for the exponential - // backoff strategy, and exposed as a parameter when adding items - // to the queue, e.g. to wait until the GC delay has passed). - // This is why we need one tree ordered by time, and one - // ordered by identifier of item to be processed (block hash). - // - // When the worker wants to process an item it takes from - // resync_queue, it checks in resync_errors that if there is an - // exponential back-off delay to await, it has passed before we - // process the item. If not, the item in the queue is skipped - // (but added back for later processing after the time of the - // delay). - // - // An alternative that would have seemed natural is to - // only add items to resync_queue with a processing time that is - // after the delay, but there are several issues with this: - // - This requires to synchronize updates to resync_queue and - // resync_errors (with the current model, there is only one thread, - // the worker thread, that accesses resync_errors, - // so no need to synchronize) by putting them both in a lock. - // This would mean that block_incref might need to take a lock - // before doing its thing, meaning it has much more chances of - // not completing successfully if something bad happens to Garage. - // Currently Garage is not able to recover from block_incref that - // doesn't complete successfully, because it is necessary to ensure - // the consistency between the state of the block manager and - // information in the BlockRef table. - // - If a resync fails, we put that block in the resync_errors table, - // and also add it back to resync_queue to be processed after - // the exponential back-off delay, - // but maybe the block is already scheduled to be resynced again - // at another time that is before the exponential back-off delay, - // and we have no way to check that easily. This means that - // in all cases, we need to check the resync_errors table - // in the resync loop at the time when a block is popped from - // the resync_queue. - // Overall, the current design is therefore simpler and more robust - // because it tolerates inconsistencies between the resync_queue - // and resync_errors table (items being scheduled in resync_queue - // for times that are earlier than the exponential back-off delay - // is a natural condition that is handled properly). - - fn spawn_background_workers(self: Arc) { - // Launch a background workers for background resync loop processing - let background = self.system.background.clone(); - let worker = ResyncWorker::new(self.clone()); - tokio::spawn(async move { - tokio::time::sleep(Duration::from_secs(10)).await; - background.spawn_worker(worker); - }); - - // Launch a background worker for data store scrubs - let (scrub_tx, scrub_rx) = mpsc::channel(1); - self.tx_scrub_command.store(Some(Arc::new(scrub_tx))); - let scrub_worker = ScrubWorker::new(self.clone(), scrub_rx); - self.system.background.spawn_worker(scrub_worker); - } - - pub(crate) fn put_to_resync(&self, hash: &Hash, delay: Duration) -> db::Result<()> { - let when = now_msec() + delay.as_millis() as u64; - self.put_to_resync_at(hash, when) - } - - fn put_to_resync_at(&self, hash: &Hash, when: u64) -> db::Result<()> { - trace!("Put resync_queue: {} {:?}", when, hash); - let mut key = u64::to_be_bytes(when).to_vec(); - key.extend(hash.as_ref()); - self.resync_queue.insert(key, hash.as_ref())?; - self.resync_notify.notify_waiters(); - Ok(()) - } - - async fn resync_iter(&self) -> Result { - if let Some((time_bytes, hash_bytes)) = self.resync_queue.first()? { - let time_msec = u64::from_be_bytes(time_bytes[0..8].try_into().unwrap()); - let now = now_msec(); - - if now >= time_msec { - let hash = Hash::try_from(&hash_bytes[..]).unwrap(); - - if let Some(ec) = self.resync_errors.get(hash.as_slice())? { - let ec = ErrorCounter::decode(&ec); - if now < ec.next_try() { - // if next retry after an error is not yet, - // don't do resync and return early, but still - // make sure the item is still in queue at expected time - self.put_to_resync_at(&hash, ec.next_try())?; - // ec.next_try() > now >= time_msec, so this remove - // is not removing the one we added just above - // (we want to do the remove after the insert to ensure - // that the item is not lost if we crash in-between) - self.resync_queue.remove(time_bytes)?; - return Ok(ResyncIterResult::BusyDidNothing); - } - } - - let tracer = opentelemetry::global::tracer("garage"); - let trace_id = gen_uuid(); - let span = tracer - .span_builder("Resync block") - .with_trace_id( - opentelemetry::trace::TraceId::from_hex(&hex::encode( - &trace_id.as_slice()[..16], - )) - .unwrap(), - ) - .with_attributes(vec![KeyValue::new("block", format!("{:?}", hash))]) - .start(&tracer); - - let res = self - .resync_block(&hash) - .with_context(Context::current_with_span(span)) - .bound_record_duration(&self.metrics.resync_duration) - .await; - - self.metrics.resync_counter.add(1); - - if let Err(e) = &res { - self.metrics.resync_error_counter.add(1); - warn!("Error when resyncing {:?}: {}", hash, e); - - let err_counter = match self.resync_errors.get(hash.as_slice())? { - Some(ec) => ErrorCounter::decode(&ec).add1(now + 1), - None => ErrorCounter::new(now + 1), - }; - - self.resync_errors - .insert(hash.as_slice(), err_counter.encode())?; - - self.put_to_resync_at(&hash, err_counter.next_try())?; - // err_counter.next_try() >= now + 1 > now, - // the entry we remove from the queue is not - // the entry we inserted with put_to_resync_at - self.resync_queue.remove(time_bytes)?; - } else { - self.resync_errors.remove(hash.as_slice())?; - self.resync_queue.remove(time_bytes)?; - } - - Ok(ResyncIterResult::BusyDidSomething) - } else { - Ok(ResyncIterResult::IdleFor(Duration::from_millis( - time_msec - now, - ))) - } - } else { - // Here we wait either for a notification that an item has been - // added to the queue, or for a constant delay of 10 secs to expire. - // The delay avoids a race condition where the notification happens - // between the time we checked the queue and the first poll - // to resync_notify.notified(): if that happens, we'll just loop - // back 10 seconds later, which is fine. - Ok(ResyncIterResult::IdleFor(Duration::from_secs(10))) - } - } - - async fn resync_block(&self, hash: &Hash) -> Result<(), Error> { - let BlockStatus { exists, needed } = self - .mutation_lock - .lock() - .await - .check_block_status(hash, self) - .await?; - - if exists != needed.is_needed() || exists != needed.is_nonzero() { - debug!( - "Resync block {:?}: exists {}, nonzero rc {}, deletable {}", - hash, - exists, - needed.is_nonzero(), - needed.is_deletable(), - ); - } - - if exists && needed.is_deletable() { - info!("Resync block {:?}: offloading and deleting", hash); - - let mut who = self.replication.write_nodes(hash); - if who.len() < self.replication.write_quorum() { - return Err(Error::Message("Not trying to offload block because we don't have a quorum of nodes to write to".to_string())); - } - who.retain(|id| *id != self.system.id); - - let msg = Arc::new(BlockRpc::NeedBlockQuery(*hash)); - let who_needs_fut = who.iter().map(|to| { - self.system.rpc.call_arc( - &self.endpoint, - *to, - msg.clone(), - RequestStrategy::with_priority(PRIO_BACKGROUND) - .with_timeout(NEED_BLOCK_QUERY_TIMEOUT), - ) - }); - let who_needs_resps = join_all(who_needs_fut).await; - - let mut need_nodes = vec![]; - for (node, needed) in who.iter().zip(who_needs_resps.into_iter()) { - match needed.err_context("NeedBlockQuery RPC")? { - BlockRpc::NeedBlockReply(needed) => { - if needed { - need_nodes.push(*node); - } - } - m => { - return Err(Error::unexpected_rpc_message(m)); - } - } - } - - if !need_nodes.is_empty() { - trace!( - "Block {:?} needed by {} nodes, sending", - hash, - need_nodes.len() - ); - - for node in need_nodes.iter() { - self.metrics - .resync_send_counter - .add(1, &[KeyValue::new("to", format!("{:?}", node))]); - } - - let put_block_message = self.read_block(hash).await?; - self.system - .rpc - .try_call_many( - &self.endpoint, - &need_nodes[..], - put_block_message, - RequestStrategy::with_priority(PRIO_BACKGROUND) - .with_quorum(need_nodes.len()) - .with_timeout(BLOCK_RW_TIMEOUT), - ) - .await - .err_context("PutBlock RPC")?; - } - info!( - "Deleting unneeded block {:?}, offload finished ({} / {})", - hash, - need_nodes.len(), - who.len() - ); - - self.mutation_lock - .lock() - .await - .delete_if_unneeded(hash, self) - .await?; - - self.rc.clear_deleted_block_rc(hash)?; - } - - if needed.is_nonzero() && !exists { - info!( - "Resync block {:?}: fetching absent but needed block (refcount > 0)", - hash - ); - - let block_data = self.rpc_get_raw_block(hash).await?; - - self.metrics.resync_recv_counter.add(1); - - self.write_block(hash, &block_data).await?; - } - - Ok(()) - } - - async fn update_resync_persisted( - &self, - update: impl Fn(&mut ResyncPersistedConfig), - ) -> Result<(), Error> { - let mut cfg: ResyncPersistedConfig = *self.resync_persisted.load().as_ref(); - update(&mut cfg); - self.resync_persister.save_async(&cfg).await?; - self.resync_persisted.store(Arc::new(cfg)); - self.resync_notify.notify_one(); - Ok(()) - } - - pub async fn set_resync_tranquility(&self, tranquility: u32) -> Result<(), Error> { - self.update_resync_persisted(|cfg| cfg.tranquility = tranquility) - .await - } } #[async_trait] @@ -766,92 +386,13 @@ impl EndpointHandler for BlockManager { } } -#[derive(Serialize, Deserialize, Clone, Copy)] -struct ResyncPersistedConfig { - tranquility: u32, -} - -struct ResyncWorker { - manager: Arc, - tranquilizer: Tranquilizer, - next_delay: Duration, -} - -impl ResyncWorker { - fn new(manager: Arc) -> Self { - Self { - manager, - tranquilizer: Tranquilizer::new(30), - next_delay: Duration::from_secs(10), - } - } -} - -#[async_trait] -impl Worker for ResyncWorker { - fn name(&self) -> String { - "Block resync worker".into() - } - - fn info(&self) -> Option { - let mut ret = vec![]; - ret.push(format!( - "tranquility = {}", - self.manager.resync_persisted.load().tranquility - )); - - let qlen = self.manager.resync_queue_len().unwrap_or(0); - if qlen > 0 { - ret.push(format!("{} blocks in queue", qlen)); - } - - let elen = self.manager.resync_errors_len().unwrap_or(0); - if elen > 0 { - ret.push(format!("{} blocks in error state", elen)); - } - - Some(ret.join(", ")) - } - - async fn work(&mut self, _must_exit: &mut watch::Receiver) -> Result { - self.tranquilizer.reset(); - match self.manager.resync_iter().await { - Ok(ResyncIterResult::BusyDidSomething) => Ok(self - .tranquilizer - .tranquilize_worker(self.manager.resync_persisted.load().tranquility)), - Ok(ResyncIterResult::BusyDidNothing) => Ok(WorkerState::Busy), - Ok(ResyncIterResult::IdleFor(delay)) => { - self.next_delay = delay; - Ok(WorkerState::Idle) - } - Err(e) => { - // The errors that we have here are only Sled errors - // We don't really know how to handle them so just ¯\_(ツ)_/¯ - // (there is kind of an assumption that Sled won't error on us, - // if it does there is not much we can do -- TODO should we just panic?) - // Here we just give the error to the worker manager, - // it will print it to the logs and increment a counter - Err(e.into()) - } - } - } - - async fn wait_for_work(&mut self, _must_exit: &watch::Receiver) -> WorkerState { - select! { - _ = tokio::time::sleep(self.next_delay) => (), - _ = self.manager.resync_notify.notified() => (), - }; - WorkerState::Busy - } -} - -struct BlockStatus { - exists: bool, - needed: RcEntry, +pub(crate) struct BlockStatus { + pub(crate) exists: bool, + pub(crate) needed: RcEntry, } impl BlockManagerLocked { - async fn check_block_status( + pub(crate) async fn check_block_status( &self, hash: &Hash, mgr: &BlockManager, @@ -938,7 +479,11 @@ impl BlockManagerLocked { Ok(()) } - async fn delete_if_unneeded(&self, hash: &Hash, mgr: &BlockManager) -> Result<(), Error> { + pub(crate) async fn delete_if_unneeded( + &self, + hash: &Hash, + mgr: &BlockManager, + ) -> Result<(), Error> { let BlockStatus { exists, needed } = self.check_block_status(hash, mgr).await?; if exists && needed.is_deletable() { @@ -952,50 +497,3 @@ impl BlockManagerLocked { Ok(()) } } - -/// Counts the number of errors when resyncing a block, -/// and the time of the last try. -/// Used to implement exponential backoff. -#[derive(Clone, Copy, Debug)] -struct ErrorCounter { - errors: u64, - last_try: u64, -} - -impl ErrorCounter { - fn new(now: u64) -> Self { - Self { - errors: 1, - last_try: now, - } - } - - fn decode(data: &[u8]) -> Self { - Self { - errors: u64::from_be_bytes(data[0..8].try_into().unwrap()), - last_try: u64::from_be_bytes(data[8..16].try_into().unwrap()), - } - } - fn encode(&self) -> Vec { - [ - u64::to_be_bytes(self.errors), - u64::to_be_bytes(self.last_try), - ] - .concat() - } - - fn add1(self, now: u64) -> Self { - Self { - errors: self.errors + 1, - last_try: now, - } - } - - fn delay_msec(&self) -> u64 { - (RESYNC_RETRY_DELAY.as_millis() as u64) - << std::cmp::min(self.errors - 1, RESYNC_RETRY_DELAY_MAX_BACKOFF_POWER) - } - fn next_try(&self) -> u64 { - self.last_try + self.delay_msec() - } -} diff --git a/src/block/repair.rs b/src/block/repair.rs index 18e1de95..e2884b69 100644 --- a/src/block/repair.rs +++ b/src/block/repair.rs @@ -112,7 +112,9 @@ impl Worker for RepairWorker { } for hash in batch_of_hashes.into_iter() { - self.manager.put_to_resync(&hash, Duration::from_secs(0))?; + self.manager + .resync + .put_to_resync(&hash, Duration::from_secs(0))?; self.next_start = Some(hash) } @@ -124,7 +126,9 @@ impl Worker for RepairWorker { // This allows us to find blocks we are storing but don't actually need, // so that we can offload them if necessary and then delete them locally. if let Some(hash) = bi.next().await? { - self.manager.put_to_resync(&hash, Duration::from_secs(0))?; + self.manager + .resync + .put_to_resync(&hash, Duration::from_secs(0))?; Ok(WorkerState::Busy) } else { Ok(WorkerState::Done) diff --git a/src/block/resync.rs b/src/block/resync.rs new file mode 100644 index 00000000..2a8184b7 --- /dev/null +++ b/src/block/resync.rs @@ -0,0 +1,536 @@ +use std::convert::TryInto; +use std::sync::Arc; +use std::time::Duration; + +use arc_swap::ArcSwap; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; + +use futures::future::*; +use tokio::select; +use tokio::sync::{watch, Notify}; + +use opentelemetry::{ + trace::{FutureExt as OtelFutureExt, TraceContextExt, Tracer}, + Context, KeyValue, +}; + +use garage_db as db; +use garage_db::counted_tree_hack::CountedTree; + +use garage_util::background::*; +use garage_util::data::*; +use garage_util::error::*; +use garage_util::metrics::RecordDuration; +use garage_util::persister::Persister; +use garage_util::time::*; +use garage_util::tranquilizer::Tranquilizer; + +use garage_rpc::system::System; +use garage_rpc::*; + +use garage_table::replication::TableReplication; + +use crate::manager::*; + +// Timeout for RPCs that ask other nodes whether they need a copy +// of a given block before we delete it locally +pub(crate) const NEED_BLOCK_QUERY_TIMEOUT: Duration = Duration::from_secs(5); + +// The delay between the time where a resync operation fails +// and the time when it is retried, with exponential backoff +// (multiplied by 2, 4, 8, 16, etc. for every consecutive failure). +pub(crate) const RESYNC_RETRY_DELAY: Duration = Duration::from_secs(60); +// The minimum retry delay is 60 seconds = 1 minute +// The maximum retry delay is 60 seconds * 2^6 = 60 seconds << 6 = 64 minutes (~1 hour) +pub(crate) const RESYNC_RETRY_DELAY_MAX_BACKOFF_POWER: u64 = 6; +// Resync tranquility is initially set to 2, but can be changed in the CLI +// and the updated version is persisted over Garage restarts +const INITIAL_RESYNC_TRANQUILITY: u32 = 2; + +pub struct BlockResyncManager { + pub(crate) queue: CountedTree, + pub(crate) notify: Notify, + pub(crate) errors: CountedTree, + + persister: Persister, + persisted: ArcSwap, +} + +#[derive(Serialize, Deserialize, Clone, Copy)] +struct ResyncPersistedConfig { + tranquility: u32, +} + +enum ResyncIterResult { + BusyDidSomething, + BusyDidNothing, + IdleFor(Duration), +} + +impl BlockResyncManager { + pub(crate) fn new(db: &db::Db, system: &System) -> Self { + let queue = db + .open_tree("block_local_resync_queue") + .expect("Unable to open block_local_resync_queue tree"); + let queue = CountedTree::new(queue).expect("Could not count block_local_resync_queue"); + + let errors = db + .open_tree("block_local_resync_errors") + .expect("Unable to open block_local_resync_errors tree"); + let errors = CountedTree::new(errors).expect("Could not count block_local_resync_errors"); + + let persister = Persister::new(&system.metadata_dir, "resync_cfg"); + let persisted = match persister.load() { + Ok(v) => v, + Err(_) => ResyncPersistedConfig { + tranquility: INITIAL_RESYNC_TRANQUILITY, + }, + }; + + Self { + queue, + notify: Notify::new(), + errors, + persister, + persisted: ArcSwap::new(Arc::new(persisted)), + } + } + + /// Get lenght of resync queue + pub fn queue_len(&self) -> Result { + // This currently can't return an error because the CountedTree hack + // doesn't error on .len(), but this will change when we remove the hack + // (hopefully someday!) + Ok(self.queue.len()) + } + + /// Get number of blocks that have an error + pub fn errors_len(&self) -> Result { + // (see queue_len comment) + Ok(self.errors.len()) + } + + // ---- Resync loop ---- + + // This part manages a queue of blocks that need to be + // "resynchronized", i.e. that need to have a check that + // they are at present if we need them, or that they are + // deleted once the garbage collection delay has passed. + // + // Here are some explanations on how the resync queue works. + // There are two Sled trees that are used to have information + // about the status of blocks that need to be resynchronized: + // + // - resync.queue: a tree that is ordered first by a timestamp + // (in milliseconds since Unix epoch) that is the time at which + // the resync must be done, and second by block hash. + // The key in this tree is just: + // concat(timestamp (8 bytes), hash (32 bytes)) + // The value is the same 32-byte hash. + // + // - resync.errors: a tree that indicates for each block + // if the last resync resulted in an error, and if so, + // the following two informations (see the ErrorCounter struct): + // - how many consecutive resync errors for this block? + // - when was the last try? + // These two informations are used to implement an + // exponential backoff retry strategy. + // The key in this tree is the 32-byte hash of the block, + // and the value is the encoded ErrorCounter value. + // + // We need to have these two trees, because the resync queue + // is not just a queue of items to process, but a set of items + // that are waiting a specific delay until we can process them + // (the delay being necessary both internally for the exponential + // backoff strategy, and exposed as a parameter when adding items + // to the queue, e.g. to wait until the GC delay has passed). + // This is why we need one tree ordered by time, and one + // ordered by identifier of item to be processed (block hash). + // + // When the worker wants to process an item it takes from + // resync.queue, it checks in resync.errors that if there is an + // exponential back-off delay to await, it has passed before we + // process the item. If not, the item in the queue is skipped + // (but added back for later processing after the time of the + // delay). + // + // An alternative that would have seemed natural is to + // only add items to resync.queue with a processing time that is + // after the delay, but there are several issues with this: + // - This requires to synchronize updates to resync.queue and + // resync.errors (with the current model, there is only one thread, + // the worker thread, that accesses resync.errors, + // so no need to synchronize) by putting them both in a lock. + // This would mean that block_incref might need to take a lock + // before doing its thing, meaning it has much more chances of + // not completing successfully if something bad happens to Garage. + // Currently Garage is not able to recover from block_incref that + // doesn't complete successfully, because it is necessary to ensure + // the consistency between the state of the block manager and + // information in the BlockRef table. + // - If a resync fails, we put that block in the resync.errors table, + // and also add it back to resync.queue to be processed after + // the exponential back-off delay, + // but maybe the block is already scheduled to be resynced again + // at another time that is before the exponential back-off delay, + // and we have no way to check that easily. This means that + // in all cases, we need to check the resync.errors table + // in the resync loop at the time when a block is popped from + // the resync.queue. + // Overall, the current design is therefore simpler and more robust + // because it tolerates inconsistencies between the resync.queue + // and resync.errors table (items being scheduled in resync.queue + // for times that are earlier than the exponential back-off delay + // is a natural condition that is handled properly). + + pub(crate) fn put_to_resync(&self, hash: &Hash, delay: Duration) -> db::Result<()> { + let when = now_msec() + delay.as_millis() as u64; + self.put_to_resync_at(hash, when) + } + + pub(crate) fn put_to_resync_at(&self, hash: &Hash, when: u64) -> db::Result<()> { + trace!("Put resync_queue: {} {:?}", when, hash); + let mut key = u64::to_be_bytes(when).to_vec(); + key.extend(hash.as_ref()); + self.queue.insert(key, hash.as_ref())?; + self.notify.notify_waiters(); + Ok(()) + } + + async fn resync_iter(&self, manager: &BlockManager) -> Result { + if let Some((time_bytes, hash_bytes)) = self.queue.first()? { + let time_msec = u64::from_be_bytes(time_bytes[0..8].try_into().unwrap()); + let now = now_msec(); + + if now >= time_msec { + let hash = Hash::try_from(&hash_bytes[..]).unwrap(); + + if let Some(ec) = self.errors.get(hash.as_slice())? { + let ec = ErrorCounter::decode(&ec); + if now < ec.next_try() { + // if next retry after an error is not yet, + // don't do resync and return early, but still + // make sure the item is still in queue at expected time + self.put_to_resync_at(&hash, ec.next_try())?; + // ec.next_try() > now >= time_msec, so this remove + // is not removing the one we added just above + // (we want to do the remove after the insert to ensure + // that the item is not lost if we crash in-between) + self.queue.remove(time_bytes)?; + return Ok(ResyncIterResult::BusyDidNothing); + } + } + + let tracer = opentelemetry::global::tracer("garage"); + let trace_id = gen_uuid(); + let span = tracer + .span_builder("Resync block") + .with_trace_id( + opentelemetry::trace::TraceId::from_hex(&hex::encode( + &trace_id.as_slice()[..16], + )) + .unwrap(), + ) + .with_attributes(vec![KeyValue::new("block", format!("{:?}", hash))]) + .start(&tracer); + + let res = self + .resync_block(manager, &hash) + .with_context(Context::current_with_span(span)) + .bound_record_duration(&manager.metrics.resync_duration) + .await; + + manager.metrics.resync_counter.add(1); + + if let Err(e) = &res { + manager.metrics.resync_error_counter.add(1); + warn!("Error when resyncing {:?}: {}", hash, e); + + let err_counter = match self.errors.get(hash.as_slice())? { + Some(ec) => ErrorCounter::decode(&ec).add1(now + 1), + None => ErrorCounter::new(now + 1), + }; + + self.errors.insert(hash.as_slice(), err_counter.encode())?; + + self.put_to_resync_at(&hash, err_counter.next_try())?; + // err_counter.next_try() >= now + 1 > now, + // the entry we remove from the queue is not + // the entry we inserted with put_to_resync_at + self.queue.remove(time_bytes)?; + } else { + self.errors.remove(hash.as_slice())?; + self.queue.remove(time_bytes)?; + } + + Ok(ResyncIterResult::BusyDidSomething) + } else { + Ok(ResyncIterResult::IdleFor(Duration::from_millis( + time_msec - now, + ))) + } + } else { + // Here we wait either for a notification that an item has been + // added to the queue, or for a constant delay of 10 secs to expire. + // The delay avoids a race condition where the notification happens + // between the time we checked the queue and the first poll + // to resync_notify.notified(): if that happens, we'll just loop + // back 10 seconds later, which is fine. + Ok(ResyncIterResult::IdleFor(Duration::from_secs(10))) + } + } + + async fn resync_block(&self, manager: &BlockManager, hash: &Hash) -> Result<(), Error> { + let BlockStatus { exists, needed } = manager + .mutation_lock + .lock() + .await + .check_block_status(hash, manager) + .await?; + + if exists != needed.is_needed() || exists != needed.is_nonzero() { + debug!( + "Resync block {:?}: exists {}, nonzero rc {}, deletable {}", + hash, + exists, + needed.is_nonzero(), + needed.is_deletable(), + ); + } + + if exists && needed.is_deletable() { + info!("Resync block {:?}: offloading and deleting", hash); + + let mut who = manager.replication.write_nodes(hash); + if who.len() < manager.replication.write_quorum() { + return Err(Error::Message("Not trying to offload block because we don't have a quorum of nodes to write to".to_string())); + } + who.retain(|id| *id != manager.system.id); + + let msg = Arc::new(BlockRpc::NeedBlockQuery(*hash)); + let who_needs_fut = who.iter().map(|to| { + manager.system.rpc.call_arc( + &manager.endpoint, + *to, + msg.clone(), + RequestStrategy::with_priority(PRIO_BACKGROUND) + .with_timeout(NEED_BLOCK_QUERY_TIMEOUT), + ) + }); + let who_needs_resps = join_all(who_needs_fut).await; + + let mut need_nodes = vec![]; + for (node, needed) in who.iter().zip(who_needs_resps.into_iter()) { + match needed.err_context("NeedBlockQuery RPC")? { + BlockRpc::NeedBlockReply(needed) => { + if needed { + need_nodes.push(*node); + } + } + m => { + return Err(Error::unexpected_rpc_message(m)); + } + } + } + + if !need_nodes.is_empty() { + trace!( + "Block {:?} needed by {} nodes, sending", + hash, + need_nodes.len() + ); + + for node in need_nodes.iter() { + manager + .metrics + .resync_send_counter + .add(1, &[KeyValue::new("to", format!("{:?}", node))]); + } + + let put_block_message = manager.read_block(hash).await?; + manager + .system + .rpc + .try_call_many( + &manager.endpoint, + &need_nodes[..], + put_block_message, + RequestStrategy::with_priority(PRIO_BACKGROUND) + .with_quorum(need_nodes.len()) + .with_timeout(BLOCK_RW_TIMEOUT), + ) + .await + .err_context("PutBlock RPC")?; + } + info!( + "Deleting unneeded block {:?}, offload finished ({} / {})", + hash, + need_nodes.len(), + who.len() + ); + + manager + .mutation_lock + .lock() + .await + .delete_if_unneeded(hash, manager) + .await?; + + manager.rc.clear_deleted_block_rc(hash)?; + } + + if needed.is_nonzero() && !exists { + info!( + "Resync block {:?}: fetching absent but needed block (refcount > 0)", + hash + ); + + let block_data = manager.rpc_get_raw_block(hash).await?; + + manager.metrics.resync_recv_counter.add(1); + + manager.write_block(hash, &block_data).await?; + } + + Ok(()) + } + + async fn update_persisted( + &self, + update: impl Fn(&mut ResyncPersistedConfig), + ) -> Result<(), Error> { + let mut cfg: ResyncPersistedConfig = *self.persisted.load().as_ref(); + update(&mut cfg); + self.persister.save_async(&cfg).await?; + self.persisted.store(Arc::new(cfg)); + self.notify.notify_one(); + Ok(()) + } + + pub async fn set_tranquility(&self, tranquility: u32) -> Result<(), Error> { + self.update_persisted(|cfg| cfg.tranquility = tranquility) + .await + } +} + +pub(crate) struct ResyncWorker { + manager: Arc, + tranquilizer: Tranquilizer, + next_delay: Duration, +} + +impl ResyncWorker { + pub(crate) fn new(manager: Arc) -> Self { + Self { + manager, + tranquilizer: Tranquilizer::new(30), + next_delay: Duration::from_secs(10), + } + } +} + +#[async_trait] +impl Worker for ResyncWorker { + fn name(&self) -> String { + "Block resync worker".into() + } + + fn info(&self) -> Option { + let mut ret = vec![]; + ret.push(format!( + "tranquility = {}", + self.manager.resync.persisted.load().tranquility + )); + + let qlen = self.manager.resync.queue_len().unwrap_or(0); + if qlen > 0 { + ret.push(format!("{} blocks in queue", qlen)); + } + + let elen = self.manager.resync.errors_len().unwrap_or(0); + if elen > 0 { + ret.push(format!("{} blocks in error state", elen)); + } + + Some(ret.join(", ")) + } + + async fn work(&mut self, _must_exit: &mut watch::Receiver) -> Result { + self.tranquilizer.reset(); + match self.manager.resync.resync_iter(&self.manager).await { + Ok(ResyncIterResult::BusyDidSomething) => Ok(self + .tranquilizer + .tranquilize_worker(self.manager.resync.persisted.load().tranquility)), + Ok(ResyncIterResult::BusyDidNothing) => Ok(WorkerState::Busy), + Ok(ResyncIterResult::IdleFor(delay)) => { + self.next_delay = delay; + Ok(WorkerState::Idle) + } + Err(e) => { + // The errors that we have here are only Sled errors + // We don't really know how to handle them so just ¯\_(ツ)_/¯ + // (there is kind of an assumption that Sled won't error on us, + // if it does there is not much we can do -- TODO should we just panic?) + // Here we just give the error to the worker manager, + // it will print it to the logs and increment a counter + Err(e.into()) + } + } + } + + async fn wait_for_work(&mut self, _must_exit: &watch::Receiver) -> WorkerState { + select! { + _ = tokio::time::sleep(self.next_delay) => (), + _ = self.manager.resync.notify.notified() => (), + }; + WorkerState::Busy + } +} + +/// Counts the number of errors when resyncing a block, +/// and the time of the last try. +/// Used to implement exponential backoff. +#[derive(Clone, Copy, Debug)] +struct ErrorCounter { + errors: u64, + last_try: u64, +} + +impl ErrorCounter { + fn new(now: u64) -> Self { + Self { + errors: 1, + last_try: now, + } + } + + fn decode(data: &[u8]) -> Self { + Self { + errors: u64::from_be_bytes(data[0..8].try_into().unwrap()), + last_try: u64::from_be_bytes(data[8..16].try_into().unwrap()), + } + } + fn encode(&self) -> Vec { + [ + u64::to_be_bytes(self.errors), + u64::to_be_bytes(self.last_try), + ] + .concat() + } + + fn add1(self, now: u64) -> Self { + Self { + errors: self.errors + 1, + last_try: now, + } + } + + fn delay_msec(&self) -> u64 { + (RESYNC_RETRY_DELAY.as_millis() as u64) + << std::cmp::min(self.errors - 1, RESYNC_RETRY_DELAY_MAX_BACKOFF_POWER) + } + fn next_try(&self) -> u64 { + self.last_try + self.delay_msec() + } +} diff --git a/src/garage/admin.rs b/src/garage/admin.rs index 1d80889c..9f4764df 100644 --- a/src/garage/admin.rs +++ b/src/garage/admin.rs @@ -781,13 +781,13 @@ impl AdminRpcHandler { writeln!( &mut ret, " resync queue length: {}", - self.garage.block_manager.resync_queue_len()? + self.garage.block_manager.resync.queue_len()? ) .unwrap(); writeln!( &mut ret, " blocks with resync errors: {}", - self.garage.block_manager.resync_errors_len()? + self.garage.block_manager.resync.errors_len()? ) .unwrap(); @@ -850,7 +850,8 @@ impl AdminRpcHandler { WorkerSetCmd::ResyncTranquility { tranquility } => { self.garage .block_manager - .set_resync_tranquility(tranquility) + .resync + .set_tranquility(tranquility) .await?; Ok(AdminRpc::Ok("Resync tranquility updated".into())) } From 5e8baa433d743a06ab3ee90f375f24c3c36fc236 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 2 Sep 2022 16:52:22 +0200 Subject: [PATCH 081/149] Make BlockManagerLocked fully private again --- src/block/manager.rs | 35 ++++++++++++++++++++++------------- src/block/resync.rs | 14 ++------------ 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/block/manager.rs b/src/block/manager.rs index efb5349c..62ef96b9 100644 --- a/src/block/manager.rs +++ b/src/block/manager.rs @@ -68,7 +68,7 @@ pub struct BlockManager { compression_level: Option, - pub(crate) mutation_lock: Mutex, + mutation_lock: Mutex, pub(crate) rc: BlockRc, pub resync: BlockResyncManager, @@ -84,7 +84,7 @@ pub struct BlockManager { // This custom struct contains functions that must only be ran // when the lock is held. We ensure that it is the case by storing // it INSIDE a Mutex. -pub(crate) struct BlockManagerLocked(); +struct BlockManagerLocked(); impl BlockManager { pub fn new( @@ -331,17 +331,30 @@ impl BlockManager { Ok(data) } - /// Check if this node should have a block, but don't actually have it - async fn need_block(&self, hash: &Hash) -> Result { - let BlockStatus { exists, needed } = self - .mutation_lock + /// Check if this node has a block and whether it needs it + pub(crate) async fn check_block_status(&self, hash: &Hash) -> Result { + self.mutation_lock .lock() .await .check_block_status(hash, self) - .await?; + .await + } + + /// Check if this node should have a block, but don't actually have it + async fn need_block(&self, hash: &Hash) -> Result { + let BlockStatus { exists, needed } = self.check_block_status(hash).await?; Ok(needed.is_nonzero() && !exists) } + /// Delete block if it is not needed anymore + pub(crate) async fn delete_if_unneeded(&self, hash: &Hash) -> Result<(), Error> { + self.mutation_lock + .lock() + .await + .delete_if_unneeded(hash, self) + .await + } + /// Utility: gives the path of the directory in which a block should be found fn block_dir(&self, hash: &Hash) -> PathBuf { let mut path = self.data_dir.clone(); @@ -392,7 +405,7 @@ pub(crate) struct BlockStatus { } impl BlockManagerLocked { - pub(crate) async fn check_block_status( + async fn check_block_status( &self, hash: &Hash, mgr: &BlockManager, @@ -479,11 +492,7 @@ impl BlockManagerLocked { Ok(()) } - pub(crate) async fn delete_if_unneeded( - &self, - hash: &Hash, - mgr: &BlockManager, - ) -> Result<(), Error> { + async fn delete_if_unneeded(&self, hash: &Hash, mgr: &BlockManager) -> Result<(), Error> { let BlockStatus { exists, needed } = self.check_block_status(hash, mgr).await?; if exists && needed.is_deletable() { diff --git a/src/block/resync.rs b/src/block/resync.rs index 2a8184b7..dab08338 100644 --- a/src/block/resync.rs +++ b/src/block/resync.rs @@ -282,12 +282,7 @@ impl BlockResyncManager { } async fn resync_block(&self, manager: &BlockManager, hash: &Hash) -> Result<(), Error> { - let BlockStatus { exists, needed } = manager - .mutation_lock - .lock() - .await - .check_block_status(hash, manager) - .await?; + let BlockStatus { exists, needed } = manager.check_block_status(hash).await?; if exists != needed.is_needed() || exists != needed.is_nonzero() { debug!( @@ -370,12 +365,7 @@ impl BlockResyncManager { who.len() ); - manager - .mutation_lock - .lock() - .await - .delete_if_unneeded(hash, manager) - .await?; + manager.delete_if_unneeded(hash).await?; manager.rc.clear_deleted_block_rc(hash)?; } From 5d4b937a00882b9bf8b36f7430f3d1fe9db58903 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 2 Sep 2022 17:18:13 +0200 Subject: [PATCH 082/149] Ability to have up to 4 concurrently working resync workers --- src/block/manager.rs | 12 +++-- src/block/resync.rs | 92 +++++++++++++++++++++++++++++++++------ src/garage/admin.rs | 8 ++++ src/garage/cli/structs.rs | 5 ++- 4 files changed, 95 insertions(+), 22 deletions(-) diff --git a/src/block/manager.rs b/src/block/manager.rs index 62ef96b9..9240db25 100644 --- a/src/block/manager.rs +++ b/src/block/manager.rs @@ -125,13 +125,11 @@ impl BlockManager { }); block_manager.endpoint.set_handler(block_manager.clone()); - // Spawn one resync worker - let background = block_manager.system.background.clone(); - let worker = ResyncWorker::new(block_manager.clone()); - tokio::spawn(async move { - tokio::time::sleep(Duration::from_secs(10)).await; - background.spawn_worker(worker); - }); + // Spawn a bunch of resync workers + for index in 0..MAX_RESYNC_WORKERS { + let worker = ResyncWorker::new(index, block_manager.clone()); + block_manager.system.background.spawn_worker(worker); + } // Spawn scrub worker let scrub_worker = ScrubWorker::new(block_manager.clone(), scrub_rx); diff --git a/src/block/resync.rs b/src/block/resync.rs index dab08338..0f358d48 100644 --- a/src/block/resync.rs +++ b/src/block/resync.rs @@ -1,5 +1,6 @@ +use std::collections::HashSet; use std::convert::TryInto; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use std::time::Duration; use arc_swap::ArcSwap; @@ -44,6 +45,9 @@ pub(crate) const RESYNC_RETRY_DELAY: Duration = Duration::from_secs(60); // The minimum retry delay is 60 seconds = 1 minute // The maximum retry delay is 60 seconds * 2^6 = 60 seconds << 6 = 64 minutes (~1 hour) pub(crate) const RESYNC_RETRY_DELAY_MAX_BACKOFF_POWER: u64 = 6; + +// No more than 4 resync workers can be running in the system +pub(crate) const MAX_RESYNC_WORKERS: usize = 4; // Resync tranquility is initially set to 2, but can be changed in the CLI // and the updated version is persisted over Garage restarts const INITIAL_RESYNC_TRANQUILITY: u32 = 2; @@ -53,12 +57,15 @@ pub struct BlockResyncManager { pub(crate) notify: Notify, pub(crate) errors: CountedTree, + busy_set: BusySet, + persister: Persister, persisted: ArcSwap, } #[derive(Serialize, Deserialize, Clone, Copy)] struct ResyncPersistedConfig { + n_workers: usize, tranquility: u32, } @@ -68,6 +75,14 @@ enum ResyncIterResult { IdleFor(Duration), } +type BusySet = Arc>>>; + +struct BusyBlock { + time_bytes: Vec, + hash_bytes: Vec, + busy_set: BusySet, +} + impl BlockResyncManager { pub(crate) fn new(db: &db::Db, system: &System) -> Self { let queue = db @@ -84,6 +99,7 @@ impl BlockResyncManager { let persisted = match persister.load() { Ok(v) => v, Err(_) => ResyncPersistedConfig { + n_workers: 1, tranquility: INITIAL_RESYNC_TRANQUILITY, }, }; @@ -92,6 +108,7 @@ impl BlockResyncManager { queue, notify: Notify::new(), errors, + busy_set: Arc::new(Mutex::new(HashSet::new())), persister, persisted: ArcSwap::new(Arc::new(persisted)), } @@ -199,12 +216,12 @@ impl BlockResyncManager { } async fn resync_iter(&self, manager: &BlockManager) -> Result { - if let Some((time_bytes, hash_bytes)) = self.queue.first()? { - let time_msec = u64::from_be_bytes(time_bytes[0..8].try_into().unwrap()); + if let Some(block) = self.get_block_to_resync()? { + let time_msec = u64::from_be_bytes(block.time_bytes[0..8].try_into().unwrap()); let now = now_msec(); if now >= time_msec { - let hash = Hash::try_from(&hash_bytes[..]).unwrap(); + let hash = Hash::try_from(&block.hash_bytes[..]).unwrap(); if let Some(ec) = self.errors.get(hash.as_slice())? { let ec = ErrorCounter::decode(&ec); @@ -217,7 +234,7 @@ impl BlockResyncManager { // is not removing the one we added just above // (we want to do the remove after the insert to ensure // that the item is not lost if we crash in-between) - self.queue.remove(time_bytes)?; + self.queue.remove(&block.time_bytes)?; return Ok(ResyncIterResult::BusyDidNothing); } } @@ -258,10 +275,10 @@ impl BlockResyncManager { // err_counter.next_try() >= now + 1 > now, // the entry we remove from the queue is not // the entry we inserted with put_to_resync_at - self.queue.remove(time_bytes)?; + self.queue.remove(&block.time_bytes)?; } else { self.errors.remove(hash.as_slice())?; - self.queue.remove(time_bytes)?; + self.queue.remove(&block.time_bytes)?; } Ok(ResyncIterResult::BusyDidSomething) @@ -281,6 +298,22 @@ impl BlockResyncManager { } } + fn get_block_to_resync(&self) -> Result, db::Error> { + let mut busy = self.busy_set.lock().unwrap(); + for it in self.queue.iter()? { + let (time_bytes, hash_bytes) = it?; + if !busy.contains(&time_bytes) { + busy.insert(time_bytes.clone()); + return Ok(Some(BusyBlock { + time_bytes, + hash_bytes, + busy_set: self.busy_set.clone(), + })); + } + } + return Ok(None); + } + async fn resync_block(&self, manager: &BlockManager, hash: &Hash) -> Result<(), Error> { let BlockStatus { exists, needed } = manager.check_block_status(hash).await?; @@ -394,25 +427,44 @@ impl BlockResyncManager { update(&mut cfg); self.persister.save_async(&cfg).await?; self.persisted.store(Arc::new(cfg)); - self.notify.notify_one(); + self.notify.notify_waiters(); Ok(()) } + pub async fn set_n_workers(&self, n_workers: usize) -> Result<(), Error> { + if n_workers < 1 || n_workers > MAX_RESYNC_WORKERS { + return Err(Error::Message(format!( + "Invalid number of resync workers, must be between 1 and {}", + MAX_RESYNC_WORKERS + ))); + } + self.update_persisted(|cfg| cfg.n_workers = n_workers).await + } + pub async fn set_tranquility(&self, tranquility: u32) -> Result<(), Error> { self.update_persisted(|cfg| cfg.tranquility = tranquility) .await } } +impl Drop for BusyBlock { + fn drop(&mut self) { + let mut busy = self.busy_set.lock().unwrap(); + busy.remove(&self.time_bytes); + } +} + pub(crate) struct ResyncWorker { + index: usize, manager: Arc, tranquilizer: Tranquilizer, next_delay: Duration, } impl ResyncWorker { - pub(crate) fn new(manager: Arc) -> Self { + pub(crate) fn new(index: usize, manager: Arc) -> Self { Self { + index, manager, tranquilizer: Tranquilizer::new(30), next_delay: Duration::from_secs(10), @@ -423,15 +475,18 @@ impl ResyncWorker { #[async_trait] impl Worker for ResyncWorker { fn name(&self) -> String { - "Block resync worker".into() + format!("Block resync worker #{}", self.index + 1) } fn info(&self) -> Option { + let persisted = self.manager.resync.persisted.load(); + + if self.index >= persisted.n_workers { + return Some("(unused)".into()); + } + let mut ret = vec![]; - ret.push(format!( - "tranquility = {}", - self.manager.resync.persisted.load().tranquility - )); + ret.push(format!("tranquility = {}", persisted.tranquility)); let qlen = self.manager.resync.queue_len().unwrap_or(0); if qlen > 0 { @@ -447,6 +502,10 @@ impl Worker for ResyncWorker { } async fn work(&mut self, _must_exit: &mut watch::Receiver) -> Result { + if self.index >= self.manager.resync.persisted.load().n_workers { + return Ok(WorkerState::Idle); + } + self.tranquilizer.reset(); match self.manager.resync.resync_iter(&self.manager).await { Ok(ResyncIterResult::BusyDidSomething) => Ok(self @@ -470,10 +529,15 @@ impl Worker for ResyncWorker { } async fn wait_for_work(&mut self, _must_exit: &watch::Receiver) -> WorkerState { + while self.index >= self.manager.resync.persisted.load().n_workers { + self.manager.resync.notify.notified().await + } + select! { _ = tokio::time::sleep(self.next_delay) => (), _ = self.manager.resync.notify.notified() => (), }; + WorkerState::Busy } } diff --git a/src/garage/admin.rs b/src/garage/admin.rs index 9f4764df..76261050 100644 --- a/src/garage/admin.rs +++ b/src/garage/admin.rs @@ -847,6 +847,14 @@ impl AdminRpcHandler { .await; Ok(AdminRpc::Ok("Scrub tranquility updated".into())) } + WorkerSetCmd::ResyncNWorkers { n_workers } => { + self.garage + .block_manager + .resync + .set_n_workers(n_workers) + .await?; + Ok(AdminRpc::Ok("Number of resync workers updated".into())) + } WorkerSetCmd::ResyncTranquility { tranquility } => { self.garage .block_manager diff --git a/src/garage/cli/structs.rs b/src/garage/cli/structs.rs index 1fba934f..0388cef5 100644 --- a/src/garage/cli/structs.rs +++ b/src/garage/cli/structs.rs @@ -524,7 +524,10 @@ pub enum WorkerSetCmd { /// Set tranquility of scrub operations #[structopt(name = "scrub-tranquility", version = version::garage())] ScrubTranquility { tranquility: u32 }, - /// Set tranquility of resync operations + /// Set number of concurrent block resync workers + #[structopt(name = "resync-n-workers", version = version::garage())] + ResyncNWorkers { n_workers: usize }, + /// Set tranquility of block resync operations #[structopt(name = "resync-tranquility", version = version::garage())] ResyncTranquility { tranquility: u32 }, } From e1751c8a9cb2a0d91b5aed636ee72ca4fa31ca68 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 2 Sep 2022 17:24:26 +0200 Subject: [PATCH 083/149] fix clippy --- src/block/resync.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/block/resync.rs b/src/block/resync.rs index 0f358d48..39e4d50f 100644 --- a/src/block/resync.rs +++ b/src/block/resync.rs @@ -311,7 +311,7 @@ impl BlockResyncManager { })); } } - return Ok(None); + Ok(None) } async fn resync_block(&self, manager: &BlockManager, hash: &Hash) -> Result<(), Error> { @@ -432,7 +432,7 @@ impl BlockResyncManager { } pub async fn set_n_workers(&self, n_workers: usize) -> Result<(), Error> { - if n_workers < 1 || n_workers > MAX_RESYNC_WORKERS { + if !(1..=MAX_RESYNC_WORKERS).contains(&n_workers) { return Err(Error::Message(format!( "Invalid number of resync workers, must be between 1 and {}", MAX_RESYNC_WORKERS From a6e40b75eabf0d6a863a91ae17f7d0ae20582d9e Mon Sep 17 00:00:00 2001 From: Jakub Jirutka Date: Sat, 3 Sep 2022 18:37:24 +0200 Subject: [PATCH 084/149] Add feature "system-libs" to enable linking against system libraries If this feature is enabled, libsodium-sys and zstd-sys will link dynamically against system-provided libraries instead of building and linking statically the bundled (possibly outdated and vulnerable) copies of them. This feature is intended mainly for linux package maintainers. --- Cargo.lock | 1 + src/block/Cargo.toml | 4 ++++ src/garage/Cargo.toml | 1 + src/rpc/Cargo.toml | 1 + 4 files changed, 7 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index fb708a8e..90d77d9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4140,4 +4140,5 @@ checksum = "2daf2f248d9ea44454bfcb2516534e8b8ad2fc91bf818a1885495fc42bc8ac9f" dependencies = [ "cc", "libc", + "pkg-config", ] diff --git a/src/block/Cargo.toml b/src/block/Cargo.toml index 2555a44a..ca0360b5 100644 --- a/src/block/Cargo.toml +++ b/src/block/Cargo.toml @@ -36,3 +36,7 @@ serde_bytes = "0.11" futures = "0.3" futures-util = "0.3" tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] } + + +[features] +system-libs = [ "zstd/pkg-config" ] diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index 8948e750..6cc93fc0 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -76,3 +76,4 @@ base64 = "0.13" [features] kubernetes-discovery = [ "garage_rpc/kubernetes-discovery" ] k2v = [ "garage_util/k2v", "garage_api/k2v" ] +system-libs = [ "garage_block/system-libs", "garage_rpc/system-libs", "sodiumoxide/use-pkg-config" ] diff --git a/src/rpc/Cargo.toml b/src/rpc/Cargo.toml index 80a1975c..309e3fc2 100644 --- a/src/rpc/Cargo.toml +++ b/src/rpc/Cargo.toml @@ -54,3 +54,4 @@ hyper = { version = "0.14", features = ["client", "http1", "runtime", "tcp"] } [features] kubernetes-discovery = [ "kube", "k8s-openapi", "openssl", "schemars" ] +system-libs = [ "sodiumoxide/use-pkg-config" ] From 7511ba5530d56a446fefe2372409d9c2ceea17c5 Mon Sep 17 00:00:00 2001 From: Jakub Jirutka Date: Sat, 3 Sep 2022 19:05:32 +0200 Subject: [PATCH 085/149] Allow linking against system-provided libsqlite Unfortunately, rusqlite uses the opposite logic for enabling/disabling bundled libraries to others (libsodium-sys, zstd-sys). Cargo features are very limited and doesn't allow to enable feature A in a dependency iff feature B is disabled. Note, lmdb-rkv-sys doesn't need any special treatment because it automatically links against system liblmdb if found via pkgconf. Linux distros should build garage with `--no-default-features --features system-libs` to disable bundled-libs and enable system-libs. --- src/db/Cargo.toml | 3 ++- src/garage/Cargo.toml | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/db/Cargo.toml b/src/db/Cargo.toml index f697054b..230fbaf9 100644 --- a/src/db/Cargo.toml +++ b/src/db/Cargo.toml @@ -22,7 +22,7 @@ hexdump = "0.1" tracing = "0.1.30" heed = "0.11" -rusqlite = { version = "0.27", features = ["bundled"] } +rusqlite = "0.27" sled = "0.34" # cli deps @@ -33,4 +33,5 @@ pretty_env_logger = { version = "0.4", optional = true } mktemp = "0.4" [features] +bundled-libs = [ "rusqlite/bundled" ] cli = ["clap", "pretty_env_logger"] diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index 6cc93fc0..e19aac50 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -74,6 +74,14 @@ base64 = "0.13" [features] +default = [ "bundled-libs" ] kubernetes-discovery = [ "garage_rpc/kubernetes-discovery" ] k2v = [ "garage_util/k2v", "garage_api/k2v" ] + +# NOTE: bundled-libs and system-libs should be treat as mutually exclusive; +# exactly one of them should be enabled. + +# Use bundled libsqlite instead of linking against system-provided. +bundled-libs = [ "garage_db/bundled-libs" ] +# Link against system-provided libsodium and libzstd. system-libs = [ "garage_block/system-libs", "garage_rpc/system-libs", "sodiumoxide/use-pkg-config" ] From fd8074ad9b026f195d72d53b96b6c1a05182069a Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 5 Sep 2022 16:09:01 +0200 Subject: [PATCH 086/149] Update .drone.yml signature --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index ce576278..277bfbc2 100644 --- a/.drone.yml +++ b/.drone.yml @@ -269,6 +269,6 @@ trigger: --- kind: signature -hmac: fa1f98f327abf88486c0c54984287285a4b951efa3776af9dd33b4d782b50815 +hmac: 362639b4c9541ad9bd06ff7f72b5235b2b0216bcb16eececd25285b6fe94ba6f ... From 729a910e14bc44925175ea8240d0c16fdfc18103 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 5 Sep 2022 16:40:13 +0200 Subject: [PATCH 087/149] Remove Heed default features --- Cargo.lock | 1 - src/db/Cargo.toml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90d77d9f..02cb9e11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1428,7 +1428,6 @@ dependencies = [ "lmdb-rkv-sys", "once_cell", "page_size", - "serde", "synchronoise", "url", ] diff --git a/src/db/Cargo.toml b/src/db/Cargo.toml index 230fbaf9..44f0be56 100644 --- a/src/db/Cargo.toml +++ b/src/db/Cargo.toml @@ -21,7 +21,7 @@ err-derive = "0.3" hexdump = "0.1" tracing = "0.1.30" -heed = "0.11" +heed = { version = "0.11", default-features = false, features = ["lmdb"] } rusqlite = "0.27" sled = "0.34" From db72812f01027ab2abd2226a0edaf3161f32e274 Mon Sep 17 00:00:00 2001 From: Jakub Jirutka Date: Sun, 4 Sep 2022 15:26:19 +0200 Subject: [PATCH 088/149] Use the new cargo feature resolver "2" Garage currently uses the legacy resolver "1". The new one is used by default if the root package specifies 'edition = 2021', which Garage does not (yet). The problem with the legacy resolver is, among others, that features enabled by dev-dependencies are propagated to normal dependencies. This affects e.g. hyper - one of the dev-dependencies enables "http2" feature that adds many extra dependencies. If we build garage without opentelemetry-otlp (this is enabled in the following commit), there's no normal dependency enabling "http2" feature. See https://doc.rust-lang.org/cargo/reference/resolver.html#feature-resolver-version-2 --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 122285db..a9fd4423 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "2" members = [ "src/db", "src/util", From e7af006c1c8211bf83b5d8abb7490ef270dd8345 Mon Sep 17 00:00:00 2001 From: Jakub Jirutka Date: Sat, 3 Sep 2022 23:40:44 +0200 Subject: [PATCH 089/149] Make OTLP exporter optional via feature "telemetry-otlp" opentelemetry-otlp add 48 (!) extra dependencies and increases the size of the garage binary by ~11 % (with fat LTO). --- src/api/Cargo.toml | 3 ++- src/garage/Cargo.toml | 4 +++- src/garage/main.rs | 1 + src/garage/server.rs | 8 +++++++- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml index db77cf38..782054bd 100644 --- a/src/api/Cargo.toml +++ b/src/api/Cargo.toml @@ -55,8 +55,9 @@ url = "2.1" opentelemetry = "0.17" opentelemetry-prometheus = "0.10" -opentelemetry-otlp = "0.10" +opentelemetry-otlp = { version = "0.10", optional = true } prometheus = "0.13" [features] k2v = [ "garage_util/k2v", "garage_model/k2v" ] +telemetry-otlp = ["opentelemetry-otlp"] diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index e19aac50..8573e2fc 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -56,7 +56,7 @@ netapp = "0.4" opentelemetry = { version = "0.17", features = [ "rt-tokio" ] } opentelemetry-prometheus = "0.10" -opentelemetry-otlp = "0.10" +opentelemetry-otlp = { version = "0.10", optional = true } prometheus = "0.13" [dev-dependencies] @@ -77,6 +77,8 @@ base64 = "0.13" default = [ "bundled-libs" ] kubernetes-discovery = [ "garage_rpc/kubernetes-discovery" ] k2v = [ "garage_util/k2v", "garage_api/k2v" ] +# Exporter for the OpenTelemetry Collector. +telemetry-otlp = [ "opentelemetry-otlp", "garage_api/telemetry-otlp" ] # NOTE: bundled-libs and system-libs should be treat as mutually exclusive; # exactly one of them should be enabled. diff --git a/src/garage/main.rs b/src/garage/main.rs index 89888884..8f0b377e 100644 --- a/src/garage/main.rs +++ b/src/garage/main.rs @@ -8,6 +8,7 @@ mod admin; mod cli; mod repair; mod server; +#[cfg(feature = "telemetry-otlp")] mod tracing_setup; use std::net::SocketAddr; diff --git a/src/garage/server.rs b/src/garage/server.rs index 6321357a..d328c044 100644 --- a/src/garage/server.rs +++ b/src/garage/server.rs @@ -15,6 +15,7 @@ use garage_web::run_web_server; use garage_api::k2v::api_server::K2VApiServer; use crate::admin::*; +#[cfg(feature = "telemetry-otlp")] use crate::tracing_setup::*; async fn wait_from(mut chan: watch::Receiver) { @@ -36,9 +37,14 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> { info!("Initializing Garage main data store..."); let garage = Garage::new(config.clone(), background)?; - info!("Initialize tracing..."); if let Some(export_to) = config.admin.trace_sink { + info!("Initialize tracing..."); + + #[cfg(feature = "telemetry-otlp")] init_tracing(&export_to, garage.system.id)?; + + #[cfg(not(feature = "telemetry-otlp"))] + warn!("Garage was built without OTLP exporter, admin.trace_sink is ignored."); } info!("Initialize Admin API server and metrics collector..."); From ea36b9ff904a8300afb8fb1601cde88c915a810f Mon Sep 17 00:00:00 2001 From: Jakub Jirutka Date: Sun, 4 Sep 2022 00:43:48 +0200 Subject: [PATCH 090/149] Allow building without Prometheus exporter (/metrics endpoint) prometheus and opentelemetry-prometheus add 7 extra dependencies in total and increases the size of the garage binary by ~7 % (with fat LTO). --- src/api/Cargo.toml | 5 ++-- src/api/admin/api_server.rs | 49 +++++++++++++++++++++++-------------- src/garage/Cargo.toml | 8 +++--- 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml index 782054bd..ce2d11c0 100644 --- a/src/api/Cargo.toml +++ b/src/api/Cargo.toml @@ -54,10 +54,11 @@ quick-xml = { version = "0.21", features = [ "serialize" ] } url = "2.1" opentelemetry = "0.17" -opentelemetry-prometheus = "0.10" +opentelemetry-prometheus = { version = "0.10", optional = true } opentelemetry-otlp = { version = "0.10", optional = true } -prometheus = "0.13" +prometheus = { version = "0.13", optional = true } [features] k2v = [ "garage_util/k2v", "garage_model/k2v" ] +metrics = [ "opentelemetry-prometheus", "prometheus" ] telemetry-otlp = ["opentelemetry-otlp"] diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs index c3b16715..d871d4e2 100644 --- a/src/api/admin/api_server.rs +++ b/src/api/admin/api_server.rs @@ -3,13 +3,14 @@ use std::sync::Arc; use async_trait::async_trait; use futures::future::Future; -use http::header::{ - ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW, CONTENT_TYPE, -}; +use http::header::{ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW}; use hyper::{Body, Request, Response}; -use opentelemetry::trace::{SpanRef, Tracer}; +use opentelemetry::trace::SpanRef; + +#[cfg(feature = "metrics")] use opentelemetry_prometheus::PrometheusExporter; +#[cfg(feature = "metrics")] use prometheus::{Encoder, TextEncoder}; use garage_model::garage::Garage; @@ -25,6 +26,7 @@ use crate::admin::router::{Authorization, Endpoint}; pub struct AdminApiServer { garage: Arc, + #[cfg(feature = "metrics")] exporter: PrometheusExporter, metrics_token: Option, admin_token: Option, @@ -32,7 +34,6 @@ pub struct AdminApiServer { impl AdminApiServer { pub fn new(garage: Arc) -> Self { - let exporter = opentelemetry_prometheus::exporter().init(); let cfg = &garage.config.admin; let metrics_token = cfg .metrics_token @@ -44,7 +45,8 @@ impl AdminApiServer { .map(|tok| format!("Bearer {}", tok)); Self { garage, - exporter, + #[cfg(feature = "metrics")] + exporter: opentelemetry_prometheus::exporter().init(), metrics_token, admin_token, } @@ -71,22 +73,31 @@ impl AdminApiServer { } fn handle_metrics(&self) -> Result, Error> { - let mut buffer = vec![]; - let encoder = TextEncoder::new(); + #[cfg(feature = "metrics")] + { + use opentelemetry::trace::Tracer; - let tracer = opentelemetry::global::tracer("garage"); - let metric_families = tracer.in_span("admin/gather_metrics", |_| { - self.exporter.registry().gather() - }); + let mut buffer = vec![]; + let encoder = TextEncoder::new(); - encoder - .encode(&metric_families, &mut buffer) - .ok_or_internal_error("Could not serialize metrics")?; + let tracer = opentelemetry::global::tracer("garage"); + let metric_families = tracer.in_span("admin/gather_metrics", |_| { + self.exporter.registry().gather() + }); - Ok(Response::builder() - .status(200) - .header(CONTENT_TYPE, encoder.format_type()) - .body(Body::from(buffer))?) + encoder + .encode(&metric_families, &mut buffer) + .ok_or_internal_error("Could not serialize metrics")?; + + Ok(Response::builder() + .status(200) + .header(http::header::CONTENT_TYPE, encoder.format_type()) + .body(Body::from(buffer))?) + } + #[cfg(not(feature = "metrics"))] + Err(Error::bad_request( + "Garage was built without the metrics feature".to_string(), + )) } } diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index 8573e2fc..553ac57a 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -55,9 +55,9 @@ tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi netapp = "0.4" opentelemetry = { version = "0.17", features = [ "rt-tokio" ] } -opentelemetry-prometheus = "0.10" +opentelemetry-prometheus = { version = "0.10", optional = true } opentelemetry-otlp = { version = "0.10", optional = true } -prometheus = "0.13" +prometheus = { version = "0.13", optional = true } [dev-dependencies] aws-sdk-s3 = "0.8" @@ -74,9 +74,11 @@ base64 = "0.13" [features] -default = [ "bundled-libs" ] +default = [ "bundled-libs", "metrics" ] kubernetes-discovery = [ "garage_rpc/kubernetes-discovery" ] k2v = [ "garage_util/k2v", "garage_api/k2v" ] +# Prometheus exporter (/metrics endpoint). +metrics = [ "garage_api/metrics", "opentelemetry-prometheus", "prometheus" ] # Exporter for the OpenTelemetry Collector. telemetry-otlp = [ "opentelemetry-otlp", "garage_api/telemetry-otlp" ] From 454d8474ef2b1364750ee96dacae0d69df583f93 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 6 Sep 2022 15:43:50 +0200 Subject: [PATCH 091/149] Fix clippy --- src/garage/server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/garage/server.rs b/src/garage/server.rs index d328c044..0851738d 100644 --- a/src/garage/server.rs +++ b/src/garage/server.rs @@ -37,11 +37,11 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> { info!("Initializing Garage main data store..."); let garage = Garage::new(config.clone(), background)?; - if let Some(export_to) = config.admin.trace_sink { + if config.admin.trace_sink.is_some() { info!("Initialize tracing..."); #[cfg(feature = "telemetry-otlp")] - init_tracing(&export_to, garage.system.id)?; + init_tracing(config.admin.trace_sink.as_ref().unwrap(), garage.system.id)?; #[cfg(not(feature = "telemetry-otlp"))] warn!("Garage was built without OTLP exporter, admin.trace_sink is ignored."); From 8d77a76df1c3c20300b0b4fe2671cd74c82606e2 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 6 Sep 2022 15:48:27 +0200 Subject: [PATCH 092/149] Update .nix files --- Cargo.nix | 281 ++++++++++++++++++++++++++---------------------- nix/compile.nix | 7 +- 2 files changed, 156 insertions(+), 132 deletions(-) diff --git a/Cargo.nix b/Cargo.nix index bb0d9bfa..6ee0b5b9 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -749,7 +749,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -1364,8 +1364,16 @@ in registry = "unknown"; src = fetchCrateLocal (workspaceSrc + "/src/garage"); features = builtins.concatLists [ + [ "bundled-libs" ] + [ "default" ] [ "k2v" ] [ "kubernetes-discovery" ] + [ "metrics" ] + [ "opentelemetry-otlp" ] + [ "opentelemetry-prometheus" ] + [ "prometheus" ] + [ "system-libs" ] + [ "telemetry-otlp" ] ]; dependencies = { async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; @@ -1420,6 +1428,11 @@ in src = fetchCrateLocal (workspaceSrc + "/src/api"); features = builtins.concatLists [ (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "k2v") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "metrics") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "opentelemetry-otlp") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "opentelemetry-prometheus") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "prometheus") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "telemetry-otlp") ]; dependencies = { ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "async_trait" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; @@ -1447,11 +1460,11 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "multer" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".multer."2.0.2" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "nom" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".nom."7.1.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "opentelemetry_otlp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-otlp."0.10.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "opentelemetry_prometheus" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-prometheus."0.10.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" then "opentelemetry_otlp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-otlp."0.10.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" then "opentelemetry_prometheus" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-prometheus."0.10.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "percent_encoding" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".percent-encoding."2.1.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "pin_project" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "prometheus" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".prometheus."0.13.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" then "prometheus" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".prometheus."0.13.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "quick_xml" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quick-xml."0.21.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "roxmltree" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".roxmltree."0.14.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; @@ -1469,25 +1482,28 @@ in version = "0.7.0"; registry = "unknown"; src = fetchCrateLocal (workspaceSrc + "/src/block"); + features = builtins.concatLists [ + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_block") "system-libs") + ]; dependencies = { - arc_swap = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; - async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; - futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; - futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; - garage_db = rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }; - garage_rpc = rustPackages."unknown".garage_rpc."0.7.0" { inherit profileName; }; - garage_table = rustPackages."unknown".garage_table."0.7.0" { inherit profileName; }; - garage_util = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; - hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; - opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; - rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; - rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; - serde_bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; - tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; - tracing = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; - zstd = rustPackages."registry+https://github.com/rust-lang/crates.io-index".zstd."0.9.2+zstd.1.5.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "arc_swap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "async_trait" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_db" else null } = rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_rpc" else null } = rustPackages."unknown".garage_rpc."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_table" else null } = rustPackages."unknown".garage_table."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "serde_bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "tracing" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "zstd" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".zstd."0.9.2+zstd.1.5.1" { inherit profileName; }; }; }); @@ -1497,6 +1513,7 @@ in registry = "unknown"; src = fetchCrateLocal (workspaceSrc + "/src/db"); features = builtins.concatLists [ + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "bundled-libs") (lib.optional (rootFeatures' ? "garage_db") "clap") (lib.optional (rootFeatures' ? "garage_db") "cli") (lib.optional (rootFeatures' ? "garage_db") "pretty_env_logger") @@ -1616,6 +1633,7 @@ in (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "kubernetes-discovery") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "openssl") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "schemars") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "system-libs") ]; dependencies = { ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "arc_swap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; @@ -1865,7 +1883,7 @@ in [ "ahash" ] [ "default" ] [ "inline-more" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "raw") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "raw") ]; dependencies = { ahash = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ahash."0.7.6" { inherit profileName; }; @@ -1908,12 +1926,8 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "269c7486ed6def5d7b59a427cec3e87b4d4dd4381d01e21c8c9f2d3985688392"; }; features = builtins.concatLists [ - [ "default" ] [ "lmdb" ] [ "lmdb-rkv-sys" ] - [ "serde" ] - [ "serde-bincode" ] - [ "serde-json" ] ]; dependencies = { bytemuck = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytemuck."1.9.1" { inherit profileName; }; @@ -1924,7 +1938,6 @@ in lmdb_sys = rustPackages."registry+https://github.com/rust-lang/crates.io-index".lmdb-rkv-sys."0.11.2" { inherit profileName; }; once_cell = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; page_size = rustPackages."registry+https://github.com/rust-lang/crates.io-index".page_size."0.4.2" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; synchronoise = rustPackages."registry+https://github.com/rust-lang/crates.io-index".synchronoise."1.0.0" { inherit profileName; }; ${ if hostPlatform.isWindows then "url" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".url."2.2.2" { inherit profileName; }; }; @@ -2092,10 +2105,10 @@ in features = builtins.concatLists [ (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "client") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "full") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "h2") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "full") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "k2v-client") "h2") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "http1") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "http2") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "k2v-client") "http2") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "runtime") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "server") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "socket2") @@ -2107,7 +2120,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_channel" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-channel."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "h2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".h2."0.3.12" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "k2v-client" then "h2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".h2."0.3.12" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "http" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "http_body" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."0.4.4" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "httparse" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".httparse."1.6.0" { inherit profileName; }; @@ -2556,14 +2569,15 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "ae0f8eafdd240b722243787b51fdaf8df6693fb8621d0f7061cdba574214cf88"; }; features = builtins.concatLists [ - [ "default" ] - [ "serde" ] - [ "std" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "serde") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "std") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "use-pkg-config") ]; dependencies = { - libc = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - libsodium_sys = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libsodium-sys."0.2.7" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "libsodium_sys" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libsodium-sys."0.2.7" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; }; }); @@ -2590,14 +2604,17 @@ in version = "0.2.7"; registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd"; }; + features = builtins.concatLists [ + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "use-pkg-config") + ]; dependencies = { - libc = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; buildDependencies = { - ${ if !(hostPlatform.parsed.abi.name == "msvc") then "cc" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".cc."1.0.73" { profileName = "__noProfile"; }; - ${ if hostPlatform.parsed.abi.name == "msvc" then "libc" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { profileName = "__noProfile"; }; - pkg_config = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".pkg-config."0.3.24" { profileName = "__noProfile"; }; - walkdir = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".walkdir."2.3.2" { profileName = "__noProfile"; }; + ${ if (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") && !(hostPlatform.parsed.abi.name == "msvc") then "cc" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".cc."1.0.73" { profileName = "__noProfile"; }; + ${ if (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") && hostPlatform.parsed.abi.name == "msvc" then "libc" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "pkg_config" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".pkg-config."0.3.24" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "walkdir" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".walkdir."2.3.2" { profileName = "__noProfile"; }; }; }); @@ -2607,16 +2624,16 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14"; }; features = builtins.concatLists [ - [ "bundled" ] - [ "bundled_bindings" ] - [ "cc" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "bundled") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "bundled_bindings") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "cc") [ "default" ] [ "min_sqlite_version_3_6_8" ] [ "pkg-config" ] [ "vcpkg" ] ]; buildDependencies = { - cc = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".cc."1.0.73" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_db" then "cc" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".cc."1.0.73" { profileName = "__noProfile"; }; pkg_config = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".pkg-config."0.3.24" { profileName = "__noProfile"; }; vcpkg = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".vcpkg."0.2.15" { profileName = "__noProfile"; }; }; @@ -2756,7 +2773,7 @@ in [ "os-poll" ] ]; dependencies = { - ${ if hostPlatform.parsed.kernel.name == "wasi" || hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "wasi" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if hostPlatform.isWindows then "miow" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".miow."0.3.7" { inherit profileName; }; ${ if hostPlatform.isWindows then "ntapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ntapi."0.3.7" { inherit profileName; }; @@ -3679,7 +3696,7 @@ in (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "getrandom") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "libc") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "rand_chacha") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "small_rng") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "small_rng") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "std") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "std_rng") ]; @@ -3872,28 +3889,28 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"; }; features = builtins.concatLists [ - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "aho-corasick") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "default") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "memchr") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "perf") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "perf-cache") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "perf-dfa") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "perf-inline") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "perf-literal") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "std") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "unicode") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "unicode-age") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "unicode-bool") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "unicode-case") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "unicode-gencat") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "unicode-perl") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "unicode-script") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web") "unicode-segment") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "aho-corasick") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "default") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "memchr") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "perf") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "perf-cache") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "perf-dfa") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "perf-inline") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "perf-literal") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_rpc") "std") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "unicode") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "unicode-age") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "unicode-bool") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "unicode-case") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "unicode-gencat") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "unicode-perl") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "unicode-script") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "unicode-segment") ]; dependencies = { - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web" then "aho_corasick" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aho-corasick."0.7.18" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_web" then "memchr" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "regex_syntax" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex-syntax."0.6.25" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" then "aho_corasick" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aho-corasick."0.7.18" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" then "memchr" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_rpc" then "regex_syntax" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex-syntax."0.6.25" { inherit profileName; }; }; }); @@ -3938,7 +3955,7 @@ in ]; dependencies = { ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" || hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "i686" || hostPlatform.parsed.cpu.name == "x86_64" || (hostPlatform.parsed.cpu.name == "aarch64" || hostPlatform.parsed.cpu.name == "armv6l" || hostPlatform.parsed.cpu.name == "armv7l") && (hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "fuchsia" || hostPlatform.parsed.kernel.name == "linux") then "spin" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".spin."0.5.2" { inherit profileName; }; untrusted = rustPackages."registry+https://github.com/rust-lang/crates.io-index".untrusted."0.7.1" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "wasm32" && hostPlatform.parsed.vendor.name == "unknown" && hostPlatform.parsed.kernel.name == "unknown" && hostPlatform.parsed.abi.name == "" then "web_sys" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.56" { inherit profileName; }; @@ -4085,8 +4102,8 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a"; }; features = builtins.concatLists [ - [ "bundled" ] - [ "modern_sqlite" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "bundled") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "modern_sqlite") ]; dependencies = { bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; @@ -4280,13 +4297,13 @@ in src = fetchCratesIo { inherit name version; sha256 = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"; }; features = builtins.concatLists [ [ "default" ] - [ "derive" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "derive") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "rc") - [ "serde_derive" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "serde_derive") [ "std" ] ]; dependencies = { - serde_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_derive."1.0.137" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "serde_derive" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_derive."1.0.137" { profileName = "__noProfile"; }; }; }); @@ -4534,7 +4551,7 @@ in ]; dependencies = { bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot_core."0.8.5" { inherit profileName; }; static_init_macro = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init_macro."1.0.2" { profileName = "__noProfile"; }; @@ -4794,8 +4811,8 @@ in (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "bytes") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "fs") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "full") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "io-std") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "full") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "k2v-client") "io-std") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "io-util") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "libc") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "macros") @@ -4804,8 +4821,8 @@ in (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "net") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "num_cpus") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "once_cell") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "parking_lot") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "process") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "parking_lot") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "k2v-client") "process") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "rt") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "rt-multi-thread") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "signal") @@ -4823,7 +4840,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "mio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".mio."0.8.2" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "num_cpus" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".num_cpus."1.13.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.12.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.12.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "pin_project_lite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; ${ if (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") && hostPlatform.isUnix then "signal_hook_registry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".signal-hook-registry."1.4.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "socket2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".socket2."0.4.4" { inherit profileName; }; @@ -4901,9 +4918,9 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0"; }; features = builtins.concatLists [ - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "codec") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "codec") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "compat") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "default") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "futures-io") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "io") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "slab") @@ -5020,43 +5037,43 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "9a89fd63ad6adf737582df5db40d286574513c69a11dac5214dc3b5603d6713e"; }; features = builtins.concatLists [ - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "__common") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "balance") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "buffer") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "default") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "discover") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "futures-core") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "futures-util") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "indexmap") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "limit") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "load") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "log") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "make") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "pin-project") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "pin-project-lite") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "rand") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "ready-cache") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "__common") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "balance") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "buffer") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "default") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "discover") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "futures-core") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "futures-util") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "indexmap") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "limit") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "load") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "log") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "make") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "pin-project") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "pin-project-lite") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "rand") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "ready-cache") (lib.optional (rootFeatures' ? "garage") "retry") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "slab") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "timeout") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "tokio") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "tokio-util") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "tracing") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "util") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "slab") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "timeout") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "tokio") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "tokio-util") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "tracing") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "util") ]; dependencies = { - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "futures_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "indexmap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".indexmap."1.8.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "pin_project" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "pin_project_lite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "slab" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".slab."0.4.5" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "tokio_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "tower_layer" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-layer."0.3.1" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "tower_service" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.1" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "tracing" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" then "futures_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" then "indexmap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".indexmap."1.8.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" then "pin_project" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" then "pin_project_lite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" then "slab" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".slab."0.4.5" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" then "tokio_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" then "tower_layer" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-layer."0.3.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" then "tower_service" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" then "tracing" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; }; }); @@ -5109,14 +5126,14 @@ in features = builtins.concatLists [ [ "attributes" ] [ "default" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "log") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "log") (lib.optional (rootFeatures' ? "garage") "log-always") [ "std" ] [ "tracing-attributes" ] ]; dependencies = { cfg_if = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "log" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" then "log" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; tracing_attributes = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-attributes."0.1.20" { profileName = "__noProfile"; }; tracing_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-core."0.1.23" { inherit profileName; }; @@ -5494,7 +5511,7 @@ in [ "std" ] (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "synchapi") [ "sysinfoapi" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "threadpoollegacyapiset") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "k2v-client") "threadpoollegacyapiset") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "timezoneapi") [ "winbase" ] [ "wincon" ] @@ -5555,10 +5572,10 @@ in ]; dependencies = { ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-uwp-windows-gnu" || hostPlatform.config == "i686-pc-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); @@ -5651,8 +5668,11 @@ in version = "0.9.2+zstd.1.5.1"; registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "2390ea1bf6c038c39674f22d95f0564725fc06034a47129179810b2fc58caa54"; }; + features = builtins.concatLists [ + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_block") "pkg-config") + ]; dependencies = { - zstd_safe = rustPackages."registry+https://github.com/rust-lang/crates.io-index".zstd-safe."4.1.3+zstd.1.5.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "zstd_safe" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".zstd-safe."4.1.3+zstd.1.5.1" { inherit profileName; }; }; }); @@ -5662,11 +5682,12 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "e99d81b99fb3c2c2c794e3fe56c305c63d5173a16a46b5850b07c935ffc7db79"; }; features = builtins.concatLists [ - [ "std" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_block") "pkg-config") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web") "std") ]; dependencies = { - libc = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - zstd_sys = rustPackages."registry+https://github.com/rust-lang/crates.io-index".zstd-sys."1.6.2+zstd.1.5.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "zstd_sys" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".zstd-sys."1.6.2+zstd.1.5.1" { inherit profileName; }; }; }); @@ -5676,13 +5697,15 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "2daf2f248d9ea44454bfcb2516534e8b8ad2fc91bf818a1885495fc42bc8ac9f"; }; features = builtins.concatLists [ - [ "std" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_block") "pkg-config") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web") "std") ]; dependencies = { - libc = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; buildDependencies = { - cc = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".cc."1.0.73" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "cc" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".cc."1.0.73" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_block" then "pkg_config" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".pkg-config."0.3.24" { profileName = "__noProfile"; }; }; }); diff --git a/nix/compile.nix b/nix/compile.nix index 7986fb0d..5707ed41 100644 --- a/nix/compile.nix +++ b/nix/compile.nix @@ -125,14 +125,15 @@ let /* [1] */ setBuildEnv = (buildEnv drv); /* [2] */ hardeningDisable = [ "pie" ]; }; + overrideArgs = old: { + /* [4] */ features = [ "bundled-libs" ] + ++ (if release then [ "kubernetes-discovery" "telemetry-otlp" "metrics" ] else []); + }; }) (pkgs.rustBuilder.rustLib.makeOverride { name = "garage_rpc"; overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; - overrideArgs = old: { - /* [4] */ features = if release then [ "kubernetes-discovery" ] else []; - }; }) (pkgs.rustBuilder.rustLib.makeOverride { From 7de53a4d66c71d9b5f22662f7bd473d055aa1c1f Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 6 Sep 2022 16:41:58 +0200 Subject: [PATCH 093/149] Force disable pkg-config for libsodum-sys and libzstd-sys --- nix/compile.nix | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/nix/compile.nix b/nix/compile.nix index 5707ed41..450b6398 100644 --- a/nix/compile.nix +++ b/nix/compile.nix @@ -118,6 +118,10 @@ let But we want to ship these additional features when we release Garage. In the end, we chose to exclude all features from debug builds while putting (all of) them in the release builds. Currently, the only feature of Garage is kubernetes-discovery from the garage_rpc crate. + + [5] We don't want libsodium-sys and zstd-sys to try to use pkgconfig to build against a system library. + However the features to do so get activated for some reason (due to a bug in cargo2nix?), + so disable them manually here. */ (pkgs.rustBuilder.rustLib.makeOverride { name = "garage"; @@ -183,6 +187,20 @@ let name = "k2v-client"; overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; }) + + (pkgs.rustBuilder.rustLib.makeOverride { + name = "libsodium-sys"; + overrideArgs = old: { + features = [ ]; /* [5] */ + }; + }) + + (pkgs.rustBuilder.rustLib.makeOverride { + name = "zstd-sys"; + overrideArgs = old: { + features = [ ]; /* [5] */ + }; + }) ]; packageFun = import ../Cargo.nix; From 48ffaaadfc790142ed9556f5227913fa8c32d2ed Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 6 Sep 2022 16:47:56 +0200 Subject: [PATCH 094/149] Bump versions to 0.8.0 (compatibility is broken already) --- Cargo.lock | 58 ++++++++++---------- Cargo.nix | 112 +++++++++++++++++++------------------- src/api/Cargo.toml | 12 ++-- src/block/Cargo.toml | 8 +-- src/garage/Cargo.toml | 16 +++--- src/k2v-client/Cargo.toml | 2 +- src/model/Cargo.toml | 10 ++-- src/rpc/Cargo.toml | 4 +- src/table/Cargo.toml | 6 +- src/util/Cargo.toml | 2 +- src/web/Cargo.toml | 10 ++-- 11 files changed, 120 insertions(+), 120 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 02cb9e11..272622e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -956,7 +956,7 @@ dependencies = [ [[package]] name = "garage" -version = "0.7.0" +version = "0.8.0" dependencies = [ "assert-json-diff", "async-trait", @@ -970,10 +970,10 @@ dependencies = [ "garage_api", "garage_block", "garage_db", - "garage_model 0.7.0", - "garage_rpc 0.7.0", - "garage_table 0.7.0", - "garage_util 0.7.0", + "garage_model 0.8.0", + "garage_rpc 0.8.0", + "garage_table 0.8.0", + "garage_util 0.8.0", "garage_web", "hex", "hmac 0.10.1", @@ -1002,7 +1002,7 @@ dependencies = [ [[package]] name = "garage_api" -version = "0.7.0" +version = "0.8.0" dependencies = [ "async-trait", "base64", @@ -1014,10 +1014,10 @@ dependencies = [ "futures", "futures-util", "garage_block", - "garage_model 0.7.0", - "garage_rpc 0.7.0", - "garage_table 0.7.0", - "garage_util 0.7.0", + "garage_model 0.8.0", + "garage_rpc 0.8.0", + "garage_table 0.8.0", + "garage_util 0.8.0", "hex", "hmac 0.10.1", "http", @@ -1047,7 +1047,7 @@ dependencies = [ [[package]] name = "garage_block" -version = "0.7.0" +version = "0.8.0" dependencies = [ "arc-swap", "async-trait", @@ -1055,9 +1055,9 @@ dependencies = [ "futures", "futures-util", "garage_db", - "garage_rpc 0.7.0", - "garage_table 0.7.0", - "garage_util 0.7.0", + "garage_rpc 0.8.0", + "garage_table 0.8.0", + "garage_util 0.8.0", "hex", "opentelemetry", "rand 0.8.5", @@ -1111,7 +1111,7 @@ dependencies = [ [[package]] name = "garage_model" -version = "0.7.0" +version = "0.8.0" dependencies = [ "arc-swap", "async-trait", @@ -1123,9 +1123,9 @@ dependencies = [ "garage_block", "garage_db", "garage_model 0.5.1", - "garage_rpc 0.7.0", - "garage_table 0.7.0", - "garage_util 0.7.0", + "garage_rpc 0.8.0", + "garage_table 0.8.0", + "garage_util 0.8.0", "hex", "netapp 0.4.4", "opentelemetry", @@ -1167,14 +1167,14 @@ dependencies = [ [[package]] name = "garage_rpc" -version = "0.7.0" +version = "0.8.0" dependencies = [ "arc-swap", "async-trait", "bytes 1.1.0", "futures", "futures-util", - "garage_util 0.7.0", + "garage_util 0.8.0", "gethostname", "hex", "hyper", @@ -1220,15 +1220,15 @@ dependencies = [ [[package]] name = "garage_table" -version = "0.7.0" +version = "0.8.0" dependencies = [ "async-trait", "bytes 1.1.0", "futures", "futures-util", "garage_db", - "garage_rpc 0.7.0", - "garage_util 0.7.0", + "garage_rpc 0.8.0", + "garage_util 0.8.0", "hexdump", "opentelemetry", "rand 0.8.5", @@ -1267,7 +1267,7 @@ dependencies = [ [[package]] name = "garage_util" -version = "0.7.0" +version = "0.8.0" dependencies = [ "async-trait", "blake2", @@ -1294,14 +1294,14 @@ dependencies = [ [[package]] name = "garage_web" -version = "0.7.0" +version = "0.8.0" dependencies = [ "err-derive 0.3.1", "futures", "garage_api", - "garage_model 0.7.0", - "garage_table 0.7.0", - "garage_util 0.7.0", + "garage_model 0.8.0", + "garage_table 0.8.0", + "garage_util 0.8.0", "http", "hyper", "opentelemetry", @@ -1740,7 +1740,7 @@ version = "0.0.1" dependencies = [ "base64", "clap 3.1.18", - "garage_util 0.7.0", + "garage_util 0.8.0", "http", "log", "rusoto_core", diff --git a/Cargo.nix b/Cargo.nix index 6ee0b5b9..5f6c84da 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -47,14 +47,14 @@ in cargo2nixVersion = "0.10.0"; workspace = { garage_db = rustPackages.unknown.garage_db."0.8.0"; - garage_util = rustPackages.unknown.garage_util."0.7.0"; - garage_rpc = rustPackages.unknown.garage_rpc."0.7.0"; - garage_table = rustPackages.unknown.garage_table."0.7.0"; - garage_block = rustPackages.unknown.garage_block."0.7.0"; - garage_model = rustPackages.unknown.garage_model."0.7.0"; - garage_api = rustPackages.unknown.garage_api."0.7.0"; - garage_web = rustPackages.unknown.garage_web."0.7.0"; - garage = rustPackages.unknown.garage."0.7.0"; + garage_util = rustPackages.unknown.garage_util."0.8.0"; + garage_rpc = rustPackages.unknown.garage_rpc."0.8.0"; + garage_table = rustPackages.unknown.garage_table."0.8.0"; + garage_block = rustPackages.unknown.garage_block."0.8.0"; + garage_model = rustPackages.unknown.garage_model."0.8.0"; + garage_api = rustPackages.unknown.garage_api."0.8.0"; + garage_web = rustPackages.unknown.garage_web."0.8.0"; + garage = rustPackages.unknown.garage."0.8.0"; k2v-client = rustPackages.unknown.k2v-client."0.0.1"; }; "registry+https://github.com/rust-lang/crates.io-index".ahash."0.7.6" = overridableMkRustCrate (profileName: rec { @@ -749,7 +749,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.config == "aarch64-linux-android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -1358,9 +1358,9 @@ in }; }); - "unknown".garage."0.7.0" = overridableMkRustCrate (profileName: rec { + "unknown".garage."0.8.0" = overridableMkRustCrate (profileName: rec { name = "garage"; - version = "0.7.0"; + version = "0.8.0"; registry = "unknown"; src = fetchCrateLocal (workspaceSrc + "/src/garage"); features = builtins.concatLists [ @@ -1381,14 +1381,14 @@ in bytesize = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytesize."1.1.0" { inherit profileName; }; futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; - garage_api = rustPackages."unknown".garage_api."0.7.0" { inherit profileName; }; - garage_block = rustPackages."unknown".garage_block."0.7.0" { inherit profileName; }; + garage_api = rustPackages."unknown".garage_api."0.8.0" { inherit profileName; }; + garage_block = rustPackages."unknown".garage_block."0.8.0" { inherit profileName; }; garage_db = rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }; - garage_model = rustPackages."unknown".garage_model."0.7.0" { inherit profileName; }; - garage_rpc = rustPackages."unknown".garage_rpc."0.7.0" { inherit profileName; }; - garage_table = rustPackages."unknown".garage_table."0.7.0" { inherit profileName; }; - garage_util = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; - garage_web = rustPackages."unknown".garage_web."0.7.0" { inherit profileName; }; + garage_model = rustPackages."unknown".garage_model."0.8.0" { inherit profileName; }; + garage_rpc = rustPackages."unknown".garage_rpc."0.8.0" { inherit profileName; }; + garage_table = rustPackages."unknown".garage_table."0.8.0" { inherit profileName; }; + garage_util = rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }; + garage_web = rustPackages."unknown".garage_web."0.8.0" { inherit profileName; }; hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; sodiumoxide = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; @@ -1421,9 +1421,9 @@ in }; }); - "unknown".garage_api."0.7.0" = overridableMkRustCrate (profileName: rec { + "unknown".garage_api."0.8.0" = overridableMkRustCrate (profileName: rec { name = "garage_api"; - version = "0.7.0"; + version = "0.8.0"; registry = "unknown"; src = fetchCrateLocal (workspaceSrc + "/src/api"); features = builtins.concatLists [ @@ -1444,11 +1444,11 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "form_urlencoded" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".form_urlencoded."1.0.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "garage_block" else null } = rustPackages."unknown".garage_block."0.7.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "garage_model" else null } = rustPackages."unknown".garage_model."0.7.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "garage_rpc" else null } = rustPackages."unknown".garage_rpc."0.7.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "garage_table" else null } = rustPackages."unknown".garage_table."0.7.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "garage_block" else null } = rustPackages."unknown".garage_block."0.8.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "garage_model" else null } = rustPackages."unknown".garage_model."0.8.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "garage_rpc" else null } = rustPackages."unknown".garage_rpc."0.8.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "garage_table" else null } = rustPackages."unknown".garage_table."0.8.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "hmac" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hmac."0.10.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "http" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; @@ -1477,9 +1477,9 @@ in }; }); - "unknown".garage_block."0.7.0" = overridableMkRustCrate (profileName: rec { + "unknown".garage_block."0.8.0" = overridableMkRustCrate (profileName: rec { name = "garage_block"; - version = "0.7.0"; + version = "0.8.0"; registry = "unknown"; src = fetchCrateLocal (workspaceSrc + "/src/block"); features = builtins.concatLists [ @@ -1492,9 +1492,9 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_db" else null } = rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_rpc" else null } = rustPackages."unknown".garage_rpc."0.7.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_table" else null } = rustPackages."unknown".garage_table."0.7.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_rpc" else null } = rustPackages."unknown".garage_rpc."0.8.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_table" else null } = rustPackages."unknown".garage_table."0.8.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; @@ -1559,9 +1559,9 @@ in }; }); - "unknown".garage_model."0.7.0" = overridableMkRustCrate (profileName: rec { + "unknown".garage_model."0.8.0" = overridableMkRustCrate (profileName: rec { name = "garage_model"; - version = "0.7.0"; + version = "0.8.0"; registry = "unknown"; src = fetchCrateLocal (workspaceSrc + "/src/model"); features = builtins.concatLists [ @@ -1575,12 +1575,12 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "err_derive" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_block" else null } = rustPackages."unknown".garage_block."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_block" else null } = rustPackages."unknown".garage_block."0.8.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_db" else null } = rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_model_050" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".garage_model."0.5.1" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_rpc" else null } = rustPackages."unknown".garage_rpc."0.7.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_table" else null } = rustPackages."unknown".garage_table."0.7.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_rpc" else null } = rustPackages."unknown".garage_rpc."0.8.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_table" else null } = rustPackages."unknown".garage_table."0.8.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; @@ -1622,9 +1622,9 @@ in }; }); - "unknown".garage_rpc."0.7.0" = overridableMkRustCrate (profileName: rec { + "unknown".garage_rpc."0.8.0" = overridableMkRustCrate (profileName: rec { name = "garage_rpc"; - version = "0.7.0"; + version = "0.8.0"; registry = "unknown"; src = fetchCrateLocal (workspaceSrc + "/src/rpc"); features = builtins.concatLists [ @@ -1641,7 +1641,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "gethostname" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".gethostname."0.2.3" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "hyper" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; @@ -1687,9 +1687,9 @@ in }; }); - "unknown".garage_table."0.7.0" = overridableMkRustCrate (profileName: rec { + "unknown".garage_table."0.8.0" = overridableMkRustCrate (profileName: rec { name = "garage_table"; - version = "0.7.0"; + version = "0.8.0"; registry = "unknown"; src = fetchCrateLocal (workspaceSrc + "/src/table"); dependencies = { @@ -1698,8 +1698,8 @@ in futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; garage_db = rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }; - garage_rpc = rustPackages."unknown".garage_rpc."0.7.0" { inherit profileName; }; - garage_util = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; + garage_rpc = rustPackages."unknown".garage_rpc."0.8.0" { inherit profileName; }; + garage_util = rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }; hexdump = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hexdump."0.1.1" { inherit profileName; }; opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; @@ -1738,9 +1738,9 @@ in }; }); - "unknown".garage_util."0.7.0" = overridableMkRustCrate (profileName: rec { + "unknown".garage_util."0.8.0" = overridableMkRustCrate (profileName: rec { name = "garage_util"; - version = "0.7.0"; + version = "0.8.0"; registry = "unknown"; src = fetchCrateLocal (workspaceSrc + "/src/util"); features = builtins.concatLists [ @@ -1771,18 +1771,18 @@ in }; }); - "unknown".garage_web."0.7.0" = overridableMkRustCrate (profileName: rec { + "unknown".garage_web."0.8.0" = overridableMkRustCrate (profileName: rec { name = "garage_web"; - version = "0.7.0"; + version = "0.8.0"; registry = "unknown"; src = fetchCrateLocal (workspaceSrc + "/src/web"); dependencies = { err_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; - garage_api = rustPackages."unknown".garage_api."0.7.0" { inherit profileName; }; - garage_model = rustPackages."unknown".garage_model."0.7.0" { inherit profileName; }; - garage_table = rustPackages."unknown".garage_table."0.7.0" { inherit profileName; }; - garage_util = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; + garage_api = rustPackages."unknown".garage_api."0.8.0" { inherit profileName; }; + garage_model = rustPackages."unknown".garage_model."0.8.0" { inherit profileName; }; + garage_table = rustPackages."unknown".garage_table."0.8.0" { inherit profileName; }; + garage_util = rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }; http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; hyper = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; @@ -2353,7 +2353,7 @@ in dependencies = { base64 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.13.0" { inherit profileName; }; clap = rustPackages."registry+https://github.com/rust-lang/crates.io-index".clap."3.1.18" { inherit profileName; }; - garage_util = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; }; + garage_util = rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }; http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; rusoto_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rusoto_core."0.48.0" { inherit profileName; }; @@ -2773,7 +2773,7 @@ in [ "os-poll" ] ]; dependencies = { - ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "wasi" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "wasi" || hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if hostPlatform.isWindows then "miow" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".miow."0.3.7" { inherit profileName; }; ${ if hostPlatform.isWindows then "ntapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ntapi."0.3.7" { inherit profileName; }; @@ -5573,9 +5573,9 @@ in dependencies = { ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml index ce2d11c0..106f9014 100644 --- a/src/api/Cargo.toml +++ b/src/api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garage_api" -version = "0.7.0" +version = "0.8.0" authors = ["Alex Auvolat "] edition = "2018" license = "AGPL-3.0" @@ -14,11 +14,11 @@ path = "lib.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -garage_model = { version = "0.7.0", path = "../model" } -garage_table = { version = "0.7.0", path = "../table" } -garage_block = { version = "0.7.0", path = "../block" } -garage_util = { version = "0.7.0", path = "../util" } -garage_rpc = { version = "0.7.0", path = "../rpc" } +garage_model = { version = "0.8.0", path = "../model" } +garage_table = { version = "0.8.0", path = "../table" } +garage_block = { version = "0.8.0", path = "../block" } +garage_util = { version = "0.8.0", path = "../util" } +garage_rpc = { version = "0.8.0", path = "../rpc" } async-trait = "0.1.7" base64 = "0.13" diff --git a/src/block/Cargo.toml b/src/block/Cargo.toml index ca0360b5..8cf5a01c 100644 --- a/src/block/Cargo.toml +++ b/src/block/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garage_block" -version = "0.7.0" +version = "0.8.0" authors = ["Alex Auvolat "] edition = "2018" license = "AGPL-3.0" @@ -15,9 +15,9 @@ path = "lib.rs" [dependencies] garage_db = { version = "0.8.0", path = "../db" } -garage_rpc = { version = "0.7.0", path = "../rpc" } -garage_util = { version = "0.7.0", path = "../util" } -garage_table = { version = "0.7.0", path = "../table" } +garage_rpc = { version = "0.8.0", path = "../rpc" } +garage_util = { version = "0.8.0", path = "../util" } +garage_table = { version = "0.8.0", path = "../table" } opentelemetry = "0.17" diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index 553ac57a..78579995 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garage" -version = "0.7.0" +version = "0.8.0" authors = ["Alex Auvolat "] edition = "2018" license = "AGPL-3.0" @@ -22,13 +22,13 @@ path = "tests/lib.rs" [dependencies] garage_db = { version = "0.8.0", path = "../db" } -garage_api = { version = "0.7.0", path = "../api" } -garage_block = { version = "0.7.0", path = "../block" } -garage_model = { version = "0.7.0", path = "../model" } -garage_rpc = { version = "0.7.0", path = "../rpc" } -garage_table = { version = "0.7.0", path = "../table" } -garage_util = { version = "0.7.0", path = "../util" } -garage_web = { version = "0.7.0", path = "../web" } +garage_api = { version = "0.8.0", path = "../api" } +garage_block = { version = "0.8.0", path = "../block" } +garage_model = { version = "0.8.0", path = "../model" } +garage_rpc = { version = "0.8.0", path = "../rpc" } +garage_table = { version = "0.8.0", path = "../table" } +garage_util = { version = "0.8.0", path = "../util" } +garage_web = { version = "0.8.0", path = "../web" } bytes = "1.0" bytesize = "1.1" diff --git a/src/k2v-client/Cargo.toml b/src/k2v-client/Cargo.toml index 2f8a2679..0f0b76ae 100644 --- a/src/k2v-client/Cargo.toml +++ b/src/k2v-client/Cargo.toml @@ -22,7 +22,7 @@ tokio = "1.17.0" # cli deps clap = { version = "3.1.18", optional = true, features = ["derive", "env"] } -garage_util = { version = "0.7.0", path = "../util", optional = true } +garage_util = { version = "0.8.0", path = "../util", optional = true } [features] diff --git a/src/model/Cargo.toml b/src/model/Cargo.toml index d908dc01..7b831538 100644 --- a/src/model/Cargo.toml +++ b/src/model/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garage_model" -version = "0.7.0" +version = "0.8.0" authors = ["Alex Auvolat "] edition = "2018" license = "AGPL-3.0" @@ -15,10 +15,10 @@ path = "lib.rs" [dependencies] garage_db = { version = "0.8.0", path = "../db" } -garage_rpc = { version = "0.7.0", path = "../rpc" } -garage_table = { version = "0.7.0", path = "../table" } -garage_block = { version = "0.7.0", path = "../block" } -garage_util = { version = "0.7.0", path = "../util" } +garage_rpc = { version = "0.8.0", path = "../rpc" } +garage_table = { version = "0.8.0", path = "../table" } +garage_block = { version = "0.8.0", path = "../block" } +garage_util = { version = "0.8.0", path = "../util" } garage_model_050 = { package = "garage_model", version = "0.5.1" } async-trait = "0.1.7" diff --git a/src/rpc/Cargo.toml b/src/rpc/Cargo.toml index 309e3fc2..21841a02 100644 --- a/src/rpc/Cargo.toml +++ b/src/rpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garage_rpc" -version = "0.7.0" +version = "0.8.0" authors = ["Alex Auvolat "] edition = "2018" license = "AGPL-3.0" @@ -14,7 +14,7 @@ path = "lib.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -garage_util = { version = "0.7.0", path = "../util" } +garage_util = { version = "0.8.0", path = "../util" } arc-swap = "1.0" bytes = "1.0" diff --git a/src/table/Cargo.toml b/src/table/Cargo.toml index 6de37cda..ae52e8d7 100644 --- a/src/table/Cargo.toml +++ b/src/table/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garage_table" -version = "0.7.0" +version = "0.8.0" authors = ["Alex Auvolat "] edition = "2018" license = "AGPL-3.0" @@ -15,8 +15,8 @@ path = "lib.rs" [dependencies] garage_db = { version = "0.8.0", path = "../db" } -garage_rpc = { version = "0.7.0", path = "../rpc" } -garage_util = { version = "0.7.0", path = "../util" } +garage_rpc = { version = "0.8.0", path = "../rpc" } +garage_util = { version = "0.8.0", path = "../util" } opentelemetry = "0.17" diff --git a/src/util/Cargo.toml b/src/util/Cargo.toml index 783fb3fc..5f3e5c57 100644 --- a/src/util/Cargo.toml +++ b/src/util/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garage_util" -version = "0.7.0" +version = "0.8.0" authors = ["Alex Auvolat "] edition = "2018" license = "AGPL-3.0" diff --git a/src/web/Cargo.toml b/src/web/Cargo.toml index 59a1231d..7bf70c55 100644 --- a/src/web/Cargo.toml +++ b/src/web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garage_web" -version = "0.7.0" +version = "0.8.0" authors = ["Alex Auvolat ", "Quentin Dufour "] edition = "2018" license = "AGPL-3.0" @@ -14,10 +14,10 @@ path = "lib.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -garage_api = { version = "0.7.0", path = "../api" } -garage_model = { version = "0.7.0", path = "../model" } -garage_util = { version = "0.7.0", path = "../util" } -garage_table = { version = "0.7.0", path = "../table" } +garage_api = { version = "0.8.0", path = "../api" } +garage_model = { version = "0.8.0", path = "../model" } +garage_util = { version = "0.8.0", path = "../util" } +garage_table = { version = "0.8.0", path = "../table" } err-derive = "0.3" tracing = "0.1.30" From b886c75450e3ee6a7c2b0a8265d7ada20a4d9d75 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 6 Sep 2022 17:09:43 +0200 Subject: [PATCH 095/149] Make all DB engines optional build features --- src/db/Cargo.toml | 8 +++++--- src/db/lib.rs | 4 ++++ src/garage/Cargo.toml | 12 ++++++++++-- src/model/Cargo.toml | 3 +++ src/model/garage.rs | 30 ++++++++++++++++++++++++++++-- 5 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/db/Cargo.toml b/src/db/Cargo.toml index 44f0be56..62dda2ca 100644 --- a/src/db/Cargo.toml +++ b/src/db/Cargo.toml @@ -21,9 +21,9 @@ err-derive = "0.3" hexdump = "0.1" tracing = "0.1.30" -heed = { version = "0.11", default-features = false, features = ["lmdb"] } -rusqlite = "0.27" -sled = "0.34" +heed = { version = "0.11", default-features = false, features = ["lmdb"], optional = true } +rusqlite = { version = "0.27", optional = true } +sled = { version = "0.34", optional = true } # cli deps clap = { version = "3.1.18", optional = true, features = ["derive", "env"] } @@ -35,3 +35,5 @@ mktemp = "0.4" [features] bundled-libs = [ "rusqlite/bundled" ] cli = ["clap", "pretty_env_logger"] +lmdb = [ "heed" ] +sqlite = [ "rusqlite" ] diff --git a/src/db/lib.rs b/src/db/lib.rs index f185114e..5304c195 100644 --- a/src/db/lib.rs +++ b/src/db/lib.rs @@ -1,8 +1,12 @@ #[macro_use] +#[cfg(feature = "sqlite")] extern crate tracing; +#[cfg(feature = "lmdb")] pub mod lmdb_adapter; +#[cfg(feature = "sled")] pub mod sled_adapter; +#[cfg(feature = "sqlite")] pub mod sqlite_adapter; pub mod counted_tree_hack; diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index 78579995..00b16ded 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -74,9 +74,17 @@ base64 = "0.13" [features] -default = [ "bundled-libs", "metrics" ] -kubernetes-discovery = [ "garage_rpc/kubernetes-discovery" ] +default = [ "bundled-libs", "metrics", "sled" ] + k2v = [ "garage_util/k2v", "garage_api/k2v" ] + +# Database engines, Sled is still our default even though we don't like it +sled = [ "garage_model/sled" ] +lmdb = [ "garage_model/lmdb" ] +sqlite = [ "garage_model/sqlite" ] + +# Automatic registration and discovery via Kubernetes API +kubernetes-discovery = [ "garage_rpc/kubernetes-discovery" ] # Prometheus exporter (/metrics endpoint). metrics = [ "garage_api/metrics", "opentelemetry-prometheus", "prometheus" ] # Exporter for the OpenTelemetry Collector. diff --git a/src/model/Cargo.toml b/src/model/Cargo.toml index 7b831538..cb0017b2 100644 --- a/src/model/Cargo.toml +++ b/src/model/Cargo.toml @@ -46,3 +46,6 @@ netapp = "0.4" [features] k2v = [ "garage_util/k2v" ] +lmdb = [ "garage_db/lmdb" ] +sled = [ "garage_db/sled" ] +sqlite = [ "garage_db/sqlite" ] diff --git a/src/model/garage.rs b/src/model/garage.rs index 15769a17..19eecb1e 100644 --- a/src/model/garage.rs +++ b/src/model/garage.rs @@ -80,6 +80,8 @@ impl Garage { let mut db_path = config.metadata_dir.clone(); std::fs::create_dir_all(&db_path).expect("Unable to create Garage meta data directory"); let db = match config.db_engine.as_str() { + // ---- Sled DB ---- + #[cfg(feature = "sled")] "sled" => { db_path.push("db"); info!("Opening Sled database at: {}", db_path.display()); @@ -91,6 +93,10 @@ impl Garage { .expect("Unable to open sled DB"); db::sled_adapter::SledDb::init(db) } + #[cfg(not(feature = "sled"))] + "sled" => return Err(Error::Message("sled db not available in this build".into())), + // ---- Sqlite DB ---- + #[cfg(feature = "sqlite")] "sqlite" | "sqlite3" | "rusqlite" => { db_path.push("db.sqlite"); info!("Opening Sqlite database at: {}", db_path.display()); @@ -98,6 +104,14 @@ impl Garage { .expect("Unable to open sqlite DB"); db::sqlite_adapter::SqliteDb::init(db) } + #[cfg(not(feature = "sqlite"))] + "sqlite" | "sqlite3" | "rusqlite" => { + return Err(Error::Message( + "sqlite db not available in this build".into(), + )) + } + // ---- LMDB DB ---- + #[cfg(feature = "lmdb")] "lmdb" | "heed" => { db_path.push("db.lmdb"); info!("Opening LMDB database at: {}", db_path.display()); @@ -111,10 +125,22 @@ impl Garage { .expect("Unable to open LMDB DB"); db::lmdb_adapter::LmdbDb::init(db) } + #[cfg(not(feature = "lmdb"))] + "lmdb" | "heed" => return Err(Error::Message("lmdb db not available in this build".into())), + // ---- Unavailable DB engine ---- e => { return Err(Error::Message(format!( - "Unsupported DB engine: {} (options: sled, sqlite, lmdb)", - e + "Unsupported DB engine: {} (options: {})", + e, + vec![ + #[cfg(feature = "sled")] + "sled", + #[cfg(feature = "sqlite")] + "sqlite", + #[cfg(feature = "lmdb")] + "lmdb", + ] + .join(", ") ))); } }; From bbb970965c41fbe5bdd90409dc8afdd589f24ed5 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 6 Sep 2022 17:16:45 +0200 Subject: [PATCH 096/149] Document available build features --- doc/book/cookbook/from-source.md | 49 +++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/doc/book/cookbook/from-source.md b/doc/book/cookbook/from-source.md index 5973d411..2b93da47 100644 --- a/doc/book/cookbook/from-source.md +++ b/doc/book/cookbook/from-source.md @@ -20,6 +20,24 @@ sudo apt-get update sudo apt-get install build-essential ``` +## Using source from the Gitea repository (recommended) + +The primary location for Garage's source code is the +[Gitea repository](https://git.deuxfleurs.fr/Deuxfleurs/garage). + +Clone the repository and build Garage with the following commands: + +```bash +git clone https://git.deuxfleurs.fr/Deuxfleurs/garage.git +cd garage +cargo build +``` + +Be careful, as this will make a debug build of Garage, which will be extremely slow! +To make a release build, invoke `cargo build --release` (this takes much longer). + +The binaries built this way are found in `target/{debug,release}/garage`. + ## Using source from `crates.io` Garage's source code is published on `crates.io`, Rust's official package repository. @@ -39,21 +57,20 @@ sudo cp $HOME/.cargo/bin/garage /usr/local/bin/garage ``` -## Using source from the Gitea repository +## Selecting features to activate in your build -The primary location for Garage's source code is the -[Gitea repository](https://git.deuxfleurs.fr/Deuxfleurs/garage). - -Clone the repository and build Garage with the following commands: - -```bash -git clone https://git.deuxfleurs.fr/Deuxfleurs/garage.git -cd garage -cargo build -``` - -Be careful, as this will make a debug build of Garage, which will be extremely slow! -To make a release build, invoke `cargo build --release` (this takes much longer). - -The binaries built this way are found in `target/{debug,release}/garage`. +Garage supports a number of compilation options in the form of Cargo features, +which can be used to provide builds adapted to your system and your use case. +The following features are available: +| Feature | Enabled | Description | +| ------- | ------- | ----------- | +| `bundled-libs` | BY DEFAULT | Use bundled version of sqlite3, zstd, lmdb and libsodium | +| `system-libs` | optional | Use system version of sqlite3, zstd, lmdb and libsodium if available (exclusive with `bundled-libs`, build using `cargo build --no-default-features --features system-libs`) | +| `k2v` | optional | Enable the experimental K2V API (if used, all nodes on your Garage cluster must have it enabled as well) | +| `kubernetes-discovery` | optional | Enable automatic registration and discovery of cluster nodes through the Kubernetes API | +| `metrics` | BY DEFAULT | Enable collection of metrics in Prometheus format on the admin API | +| `telemetry-otlp` | optional | Enable collection of execution traces using OpenTelemetry | +| `sled` | BY DEFAULT | Enable using Sled to store Garage's metadata | +| `lmdb` | optional | Enable using LMDB to store Garage's metadata | +| `sqlite` | optional | Enable using Sqlite3 to store Garage's metadata | From 2c2b93acdf3db1d6c379964f557abf082df269b9 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 6 Sep 2022 17:20:10 +0200 Subject: [PATCH 097/149] Update Nix files with optional db engines --- Cargo.nix | 69 ++++++++++++++++++++++++++++--------------------- nix/compile.nix | 5 ++-- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/Cargo.nix b/Cargo.nix index 5f6c84da..1cc77969 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -749,7 +749,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.config == "aarch64-linux-android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -838,7 +838,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"; }; features = builtins.concatLists [ - [ "default" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web") "default") [ "lazy_static" ] [ "std" ] ]; @@ -1368,10 +1368,13 @@ in [ "default" ] [ "k2v" ] [ "kubernetes-discovery" ] + [ "lmdb" ] [ "metrics" ] [ "opentelemetry-otlp" ] [ "opentelemetry-prometheus" ] [ "prometheus" ] + [ "sled" ] + [ "sqlite" ] [ "system-libs" ] [ "telemetry-otlp" ] ]; @@ -1516,16 +1519,21 @@ in (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "bundled-libs") (lib.optional (rootFeatures' ? "garage_db") "clap") (lib.optional (rootFeatures' ? "garage_db") "cli") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model") "heed") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model") "lmdb") (lib.optional (rootFeatures' ? "garage_db") "pretty_env_logger") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model") "rusqlite") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model") "sled") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model") "sqlite") ]; dependencies = { ${ if rootFeatures' ? "garage_db" then "clap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".clap."3.1.18" { inherit profileName; }; err_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; - heed = rustPackages."registry+https://github.com/rust-lang/crates.io-index".heed."0.11.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model" then "heed" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".heed."0.11.0" { inherit profileName; }; hexdump = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hexdump."0.1.1" { inherit profileName; }; ${ if rootFeatures' ? "garage_db" then "pretty_env_logger" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pretty_env_logger."0.4.0" { inherit profileName; }; - rusqlite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rusqlite."0.27.0" { inherit profileName; }; - sled = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model" then "rusqlite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rusqlite."0.27.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model" then "sled" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; tracing = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; }; devDependencies = { @@ -1566,6 +1574,9 @@ in src = fetchCrateLocal (workspaceSrc + "/src/model"); features = builtins.concatLists [ (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model") "k2v") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_model") "lmdb") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_model") "sled") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_model") "sqlite") ]; dependencies = { ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "arc_swap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; @@ -1880,13 +1891,13 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"; }; features = builtins.concatLists [ - [ "ahash" ] - [ "default" ] - [ "inline-more" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model") "ahash") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model") "default") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model") "inline-more") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "raw") ]; dependencies = { - ahash = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ahash."0.7.6" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model" then "ahash" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ahash."0.7.6" { inherit profileName; }; }; }); @@ -2627,15 +2638,15 @@ in (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "bundled") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "bundled_bindings") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "cc") - [ "default" ] - [ "min_sqlite_version_3_6_8" ] - [ "pkg-config" ] - [ "vcpkg" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model") "default") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model") "min_sqlite_version_3_6_8") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model") "pkg-config") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model") "vcpkg") ]; buildDependencies = { ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_db" then "cc" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".cc."1.0.73" { profileName = "__noProfile"; }; - pkg_config = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".pkg-config."0.3.24" { profileName = "__noProfile"; }; - vcpkg = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".vcpkg."0.2.15" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model" then "pkg_config" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".pkg-config."0.3.24" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model" then "vcpkg" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".vcpkg."0.2.15" { profileName = "__noProfile"; }; }; }); @@ -2773,7 +2784,7 @@ in [ "os-poll" ] ]; dependencies = { - ${ if hostPlatform.parsed.kernel.name == "wasi" || hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "wasi" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if hostPlatform.isWindows then "miow" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".miow."0.3.7" { inherit profileName; }; ${ if hostPlatform.isWindows then "ntapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ntapi."0.3.7" { inherit profileName; }; @@ -3955,7 +3966,7 @@ in ]; dependencies = { ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" || hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "i686" || hostPlatform.parsed.cpu.name == "x86_64" || (hostPlatform.parsed.cpu.name == "aarch64" || hostPlatform.parsed.cpu.name == "armv6l" || hostPlatform.parsed.cpu.name == "armv7l") && (hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "fuchsia" || hostPlatform.parsed.kernel.name == "linux") then "spin" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".spin."0.5.2" { inherit profileName; }; untrusted = rustPackages."registry+https://github.com/rust-lang/crates.io-index".untrusted."0.7.1" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "wasm32" && hostPlatform.parsed.vendor.name == "unknown" && hostPlatform.parsed.kernel.name == "unknown" && hostPlatform.parsed.abi.name == "" then "web_sys" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.56" { inherit profileName; }; @@ -4106,13 +4117,13 @@ in (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "modern_sqlite") ]; dependencies = { - bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; - fallible_iterator = rustPackages."registry+https://github.com/rust-lang/crates.io-index".fallible-iterator."0.2.0" { inherit profileName; }; - fallible_streaming_iterator = rustPackages."registry+https://github.com/rust-lang/crates.io-index".fallible-streaming-iterator."0.1.9" { inherit profileName; }; - hashlink = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hashlink."0.7.0" { inherit profileName; }; - libsqlite3_sys = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libsqlite3-sys."0.24.2" { inherit profileName; }; - memchr = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; }; - smallvec = rustPackages."registry+https://github.com/rust-lang/crates.io-index".smallvec."1.8.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model" then "bitflags" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model" then "fallible_iterator" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".fallible-iterator."0.2.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model" then "fallible_streaming_iterator" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".fallible-streaming-iterator."0.1.9" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model" then "hashlink" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hashlink."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model" then "libsqlite3_sys" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libsqlite3-sys."0.24.2" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model" then "memchr" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model" then "smallvec" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".smallvec."1.8.0" { inherit profileName; }; }; }); @@ -5499,10 +5510,10 @@ in (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "namedpipeapi") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "ntdef") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "ntsecapi") - [ "ntstatus" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web") "ntstatus") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "objbase") [ "processenv" ] - [ "processthreadsapi" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web") "processthreadsapi") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "profileapi") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "schannel") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "securitybaseapi") @@ -5571,9 +5582,9 @@ in [ "default" ] ]; dependencies = { - ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-uwp-windows-gnu" || hostPlatform.config == "i686-pc-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; diff --git a/nix/compile.nix b/nix/compile.nix index 450b6398..ea431a7e 100644 --- a/nix/compile.nix +++ b/nix/compile.nix @@ -117,7 +117,6 @@ let It speeds up the compilation (when the feature is not required) and released crates have less dependency by default (less attack surface, disk space, etc.). But we want to ship these additional features when we release Garage. In the end, we chose to exclude all features from debug builds while putting (all of) them in the release builds. - Currently, the only feature of Garage is kubernetes-discovery from the garage_rpc crate. [5] We don't want libsodium-sys and zstd-sys to try to use pkgconfig to build against a system library. However the features to do so get activated for some reason (due to a bug in cargo2nix?), @@ -130,8 +129,8 @@ let /* [2] */ hardeningDisable = [ "pie" ]; }; overrideArgs = old: { - /* [4] */ features = [ "bundled-libs" ] - ++ (if release then [ "kubernetes-discovery" "telemetry-otlp" "metrics" ] else []); + /* [4] */ features = [ "bundled-libs" "sled" ] + ++ (if release then [ "kubernetes-discovery" "telemetry-otlp" "metrics" "lmdb" "sqlite" ] else []); }; }) From 431dee050f9dd1454ac89d20de214f973cbb387f Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 6 Sep 2022 17:25:44 +0200 Subject: [PATCH 098/149] Remove opentelemetry-otlp dep in api/ --- Cargo.lock | 1 - Cargo.nix | 151 +++++++++++++++++++++--------------------- src/api/Cargo.toml | 2 - src/garage/Cargo.toml | 2 +- 4 files changed, 75 insertions(+), 81 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 272622e1..0933cc41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1029,7 +1029,6 @@ dependencies = [ "multer", "nom", "opentelemetry", - "opentelemetry-otlp", "opentelemetry-prometheus", "percent-encoding", "pin-project 1.0.10", diff --git a/Cargo.nix b/Cargo.nix index 1cc77969..2b500857 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -1432,10 +1432,8 @@ in features = builtins.concatLists [ (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "k2v") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "metrics") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "opentelemetry-otlp") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "opentelemetry-prometheus") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "prometheus") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "telemetry-otlp") ]; dependencies = { ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "async_trait" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; @@ -1463,7 +1461,6 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "multer" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".multer."2.0.2" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "nom" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".nom."7.1.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" then "opentelemetry_otlp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-otlp."0.10.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" then "opentelemetry_prometheus" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-prometheus."0.10.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "percent_encoding" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".percent-encoding."2.1.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "pin_project" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; }; @@ -1894,7 +1891,7 @@ in (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model") "ahash") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model") "default") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model") "inline-more") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "raw") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "raw") ]; dependencies = { ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model" then "ahash" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ahash."0.7.6" { inherit profileName; }; @@ -2116,10 +2113,10 @@ in features = builtins.concatLists [ (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "client") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "full") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "k2v-client") "h2") + (lib.optional (rootFeatures' ? "garage") "full") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "k2v-client") "h2") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "http1") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "k2v-client") "http2") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "k2v-client") "http2") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "runtime") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "server") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "socket2") @@ -2131,7 +2128,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_channel" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-channel."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "k2v-client" then "h2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".h2."0.3.12" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "k2v-client" then "h2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".h2."0.3.12" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "http" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "http_body" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."0.4.4" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "httparse" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".httparse."1.6.0" { inherit profileName; }; @@ -2784,7 +2781,7 @@ in [ "os-poll" ] ]; dependencies = { - ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "wasi" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "wasi" || hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if hostPlatform.isWindows then "miow" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".miow."0.3.7" { inherit profileName; }; ${ if hostPlatform.isWindows then "ntapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ntapi."0.3.7" { inherit profileName; }; @@ -3707,7 +3704,7 @@ in (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "getrandom") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "libc") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "rand_chacha") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "small_rng") + (lib.optional (rootFeatures' ? "garage") "small_rng") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "std") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "std_rng") ]; @@ -3900,28 +3897,28 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"; }; features = builtins.concatLists [ - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "aho-corasick") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "default") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "memchr") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "perf") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "perf-cache") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "perf-dfa") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "perf-inline") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "perf-literal") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_rpc") "std") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "unicode") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "unicode-age") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "unicode-bool") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "unicode-case") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "unicode-gencat") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "unicode-perl") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "unicode-script") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db") "unicode-segment") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "aho-corasick") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "default") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "memchr") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "perf") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "perf-cache") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "perf-dfa") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "perf-inline") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "perf-literal") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_rpc") "std") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "unicode") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "unicode-age") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "unicode-bool") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "unicode-case") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "unicode-gencat") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "unicode-perl") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "unicode-script") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db") "unicode-segment") ]; dependencies = { - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" then "aho_corasick" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aho-corasick."0.7.18" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" then "memchr" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_rpc" then "regex_syntax" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex-syntax."0.6.25" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_db" then "aho_corasick" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aho-corasick."0.7.18" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_db" then "memchr" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_rpc" then "regex_syntax" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex-syntax."0.6.25" { inherit profileName; }; }; }); @@ -4562,7 +4559,7 @@ in ]; dependencies = { bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot_core."0.8.5" { inherit profileName; }; static_init_macro = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init_macro."1.0.2" { profileName = "__noProfile"; }; @@ -4822,8 +4819,8 @@ in (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "bytes") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "fs") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "full") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "k2v-client") "io-std") + (lib.optional (rootFeatures' ? "garage") "full") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "k2v-client") "io-std") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "io-util") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "libc") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "macros") @@ -4832,8 +4829,8 @@ in (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "net") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "num_cpus") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "once_cell") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "parking_lot") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "k2v-client") "process") + (lib.optional (rootFeatures' ? "garage") "parking_lot") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "k2v-client") "process") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "rt") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "rt-multi-thread") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "signal") @@ -4851,7 +4848,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "mio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".mio."0.8.2" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "num_cpus" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".num_cpus."1.13.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.12.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.12.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "pin_project_lite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; ${ if (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") && hostPlatform.isUnix then "signal_hook_registry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".signal-hook-registry."1.4.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "socket2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".socket2."0.4.4" { inherit profileName; }; @@ -4929,9 +4926,9 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0"; }; features = builtins.concatLists [ - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "codec") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "codec") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "compat") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "default") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "default") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "futures-io") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "io") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "slab") @@ -5048,43 +5045,43 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "9a89fd63ad6adf737582df5db40d286574513c69a11dac5214dc3b5603d6713e"; }; features = builtins.concatLists [ - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "__common") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "balance") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "buffer") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "default") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "discover") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "futures-core") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "futures-util") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "indexmap") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "limit") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "load") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "log") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "make") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "pin-project") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "pin-project-lite") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "rand") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "ready-cache") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "__common") + (lib.optional (rootFeatures' ? "garage") "balance") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "buffer") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "default") + (lib.optional (rootFeatures' ? "garage") "discover") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "futures-core") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "futures-util") + (lib.optional (rootFeatures' ? "garage") "indexmap") + (lib.optional (rootFeatures' ? "garage") "limit") + (lib.optional (rootFeatures' ? "garage") "load") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "log") + (lib.optional (rootFeatures' ? "garage") "make") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "pin-project") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "pin-project-lite") + (lib.optional (rootFeatures' ? "garage") "rand") + (lib.optional (rootFeatures' ? "garage") "ready-cache") (lib.optional (rootFeatures' ? "garage") "retry") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "slab") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api") "timeout") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "tokio") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "tokio-util") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "tracing") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "util") + (lib.optional (rootFeatures' ? "garage") "slab") + (lib.optional (rootFeatures' ? "garage") "timeout") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "tokio") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "tokio-util") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "tracing") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "util") ]; dependencies = { - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" then "futures_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" then "indexmap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".indexmap."1.8.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" then "pin_project" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" then "pin_project_lite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" then "slab" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".slab."0.4.5" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" then "tokio_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" then "tower_layer" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-layer."0.3.1" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" then "tower_service" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.1" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" then "tracing" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "futures_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" then "indexmap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".indexmap."1.8.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "pin_project" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "pin_project_lite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; + ${ if rootFeatures' ? "garage" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; + ${ if rootFeatures' ? "garage" then "slab" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".slab."0.4.5" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "tokio_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "tower_layer" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-layer."0.3.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "tower_service" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "tracing" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; }; }); @@ -5137,14 +5134,14 @@ in features = builtins.concatLists [ [ "attributes" ] [ "default" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc") "log") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "log") (lib.optional (rootFeatures' ? "garage") "log-always") [ "std" ] [ "tracing-attributes" ] ]; dependencies = { cfg_if = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" then "log" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "log" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; tracing_attributes = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-attributes."0.1.20" { profileName = "__noProfile"; }; tracing_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-core."0.1.23" { inherit profileName; }; @@ -5522,7 +5519,7 @@ in [ "std" ] (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "synchapi") [ "sysinfoapi" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "k2v-client") "threadpoollegacyapiset") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "k2v-client") "threadpoollegacyapiset") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "timezoneapi") [ "winbase" ] [ "wincon" ] @@ -5582,10 +5579,10 @@ in [ "default" ] ]; dependencies = { - ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-uwp-windows-gnu" || hostPlatform.config == "i686-pc-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml index 106f9014..eb87a4fb 100644 --- a/src/api/Cargo.toml +++ b/src/api/Cargo.toml @@ -55,10 +55,8 @@ url = "2.1" opentelemetry = "0.17" opentelemetry-prometheus = { version = "0.10", optional = true } -opentelemetry-otlp = { version = "0.10", optional = true } prometheus = { version = "0.13", optional = true } [features] k2v = [ "garage_util/k2v", "garage_model/k2v" ] metrics = [ "opentelemetry-prometheus", "prometheus" ] -telemetry-otlp = ["opentelemetry-otlp"] diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index 00b16ded..b08e9439 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -88,7 +88,7 @@ kubernetes-discovery = [ "garage_rpc/kubernetes-discovery" ] # Prometheus exporter (/metrics endpoint). metrics = [ "garage_api/metrics", "opentelemetry-prometheus", "prometheus" ] # Exporter for the OpenTelemetry Collector. -telemetry-otlp = [ "opentelemetry-otlp", "garage_api/telemetry-otlp" ] +telemetry-otlp = [ "opentelemetry-otlp" ] # NOTE: bundled-libs and system-libs should be treat as mutually exclusive; # exactly one of them should be enabled. From 1e92e9f78251ed72e3e5eb27ed3f389f9f53c488 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 6 Sep 2022 17:29:46 +0200 Subject: [PATCH 099/149] Disable k2v tests when feature is disabled --- src/garage/tests/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/garage/tests/lib.rs b/src/garage/tests/lib.rs index 0106ad10..d15639b9 100644 --- a/src/garage/tests/lib.rs +++ b/src/garage/tests/lib.rs @@ -3,5 +3,6 @@ mod common; mod admin; mod bucket; +#[cfg(feature = "k2v")] mod k2v; mod s3; From 0f5689c16920479066277db2880e2ca87f7ca602 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 6 Sep 2022 17:52:50 +0200 Subject: [PATCH 100/149] Include code from v0.5.1 directly to remove dependencies --- Cargo.lock | 173 ++++----------------------- Cargo.nix | 143 +--------------------- src/model/Cargo.toml | 1 - src/model/key_table.rs | 2 +- src/model/lib.rs | 3 + src/model/migrate.rs | 2 +- src/model/prev/mod.rs | 1 + src/model/prev/v051/bucket_table.rs | 63 ++++++++++ src/model/prev/v051/key_table.rs | 51 ++++++++ src/model/prev/v051/mod.rs | 4 + src/model/prev/v051/object_table.rs | 150 +++++++++++++++++++++++ src/model/prev/v051/version_table.rs | 79 ++++++++++++ src/model/s3/object_table.rs | 2 +- src/model/s3/version_table.rs | 2 +- 14 files changed, 386 insertions(+), 290 deletions(-) create mode 100644 src/model/prev/mod.rs create mode 100644 src/model/prev/v051/bucket_table.rs create mode 100644 src/model/prev/v051/key_table.rs create mode 100644 src/model/prev/v051/mod.rs create mode 100644 src/model/prev/v051/object_table.rs create mode 100644 src/model/prev/v051/version_table.rs diff --git a/Cargo.lock b/Cargo.lock index 0933cc41..976acfc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -970,17 +970,17 @@ dependencies = [ "garage_api", "garage_block", "garage_db", - "garage_model 0.8.0", - "garage_rpc 0.8.0", - "garage_table 0.8.0", - "garage_util 0.8.0", + "garage_model", + "garage_rpc", + "garage_table", + "garage_util", "garage_web", "hex", "hmac 0.10.1", "http", "hyper", "kuska-sodiumoxide", - "netapp 0.4.4", + "netapp", "opentelemetry", "opentelemetry-otlp", "opentelemetry-prometheus", @@ -1014,10 +1014,10 @@ dependencies = [ "futures", "futures-util", "garage_block", - "garage_model 0.8.0", - "garage_rpc 0.8.0", - "garage_table 0.8.0", - "garage_util 0.8.0", + "garage_model", + "garage_rpc", + "garage_table", + "garage_util", "hex", "hmac 0.10.1", "http", @@ -1054,9 +1054,9 @@ dependencies = [ "futures", "futures-util", "garage_db", - "garage_rpc 0.8.0", - "garage_table 0.8.0", - "garage_util 0.8.0", + "garage_rpc", + "garage_table", + "garage_util", "hex", "opentelemetry", "rand 0.8.5", @@ -1083,31 +1083,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "garage_model" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "584619e8999713d73761775591ad6f01ff8c9d724f3b20984f5932f1fc7f9988" -dependencies = [ - "arc-swap", - "async-trait", - "futures", - "futures-util", - "garage_rpc 0.5.1", - "garage_table 0.5.1", - "garage_util 0.5.1", - "hex", - "log", - "netapp 0.3.1", - "rand 0.8.5", - "rmp-serde 0.15.5", - "serde", - "serde_bytes", - "sled", - "tokio", - "zstd", -] - [[package]] name = "garage_model" version = "0.8.0" @@ -1121,12 +1096,11 @@ dependencies = [ "futures-util", "garage_block", "garage_db", - "garage_model 0.5.1", - "garage_rpc 0.8.0", - "garage_table 0.8.0", - "garage_util 0.8.0", + "garage_rpc", + "garage_table", + "garage_util", "hex", - "netapp 0.4.4", + "netapp", "opentelemetry", "rand 0.8.5", "rmp-serde 0.15.5", @@ -1137,33 +1111,6 @@ dependencies = [ "zstd", ] -[[package]] -name = "garage_rpc" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81e693aa4582cfe7a7ce70c07880e3662544b5d0cd68bc4b59c53febfbb8d1ec" -dependencies = [ - "arc-swap", - "async-trait", - "bytes 1.1.0", - "futures", - "futures-util", - "garage_util 0.5.1", - "gethostname", - "hex", - "hyper", - "kuska-sodiumoxide", - "log", - "netapp 0.3.1", - "rand 0.8.5", - "rmp-serde 0.15.5", - "serde", - "serde_bytes", - "serde_json", - "tokio", - "tokio-stream", -] - [[package]] name = "garage_rpc" version = "0.8.0" @@ -1173,14 +1120,14 @@ dependencies = [ "bytes 1.1.0", "futures", "futures-util", - "garage_util 0.8.0", + "garage_util", "gethostname", "hex", "hyper", "k8s-openapi", "kube", "kuska-sodiumoxide", - "netapp 0.4.4", + "netapp", "openssl", "opentelemetry", "pnet_datalink", @@ -1195,28 +1142,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "garage_table" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3557f3757e2acd29eaee86804d4e6c38d2abda81b4b349d8a0d2277044265c" -dependencies = [ - "async-trait", - "bytes 1.1.0", - "futures", - "futures-util", - "garage_rpc 0.5.1", - "garage_util 0.5.1", - "hexdump", - "log", - "rand 0.8.5", - "rmp-serde 0.15.5", - "serde", - "serde_bytes", - "sled", - "tokio", -] - [[package]] name = "garage_table" version = "0.8.0" @@ -1226,8 +1151,8 @@ dependencies = [ "futures", "futures-util", "garage_db", - "garage_rpc 0.8.0", - "garage_util 0.8.0", + "garage_rpc", + "garage_util", "hexdump", "opentelemetry", "rand 0.8.5", @@ -1238,32 +1163,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "garage_util" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e096994382447431e2f3c70e3685eb8b24c00eceff8667bb22a2a27ff17832f" -dependencies = [ - "blake2", - "chrono", - "err-derive 0.3.1", - "futures", - "hex", - "http", - "hyper", - "log", - "netapp 0.3.1", - "rand 0.8.5", - "rmp-serde 0.15.5", - "serde", - "serde_json", - "sha2", - "sled", - "tokio", - "toml", - "xxhash-rust", -] - [[package]] name = "garage_util" version = "0.8.0" @@ -1278,7 +1177,7 @@ dependencies = [ "hex", "http", "hyper", - "netapp 0.4.4", + "netapp", "opentelemetry", "rand 0.8.5", "rmp-serde 0.15.5", @@ -1298,9 +1197,9 @@ dependencies = [ "err-derive 0.3.1", "futures", "garage_api", - "garage_model 0.8.0", - "garage_table 0.8.0", - "garage_util 0.8.0", + "garage_model", + "garage_table", + "garage_util", "http", "hyper", "opentelemetry", @@ -1739,7 +1638,7 @@ version = "0.0.1" dependencies = [ "base64", "clap 3.1.18", - "garage_util 0.8.0", + "garage_util", "http", "log", "rusoto_core", @@ -2085,28 +1984,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "netapp" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac7dcd2c6c5a9d5ea88ffc17d3339d49d308e68c4d8190c494b55ddbf78d80ad" -dependencies = [ - "arc-swap", - "async-trait", - "bytes 0.6.0", - "err-derive 0.2.4", - "futures", - "hex", - "kuska-handshake", - "kuska-sodiumoxide", - "log", - "rmp-serde 0.14.4", - "serde", - "tokio", - "tokio-stream", - "tokio-util 0.6.9", -] - [[package]] name = "netapp" version = "0.4.4" diff --git a/Cargo.nix b/Cargo.nix index 2b500857..614050c8 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -749,7 +749,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.config == "aarch64-linux-android" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -838,7 +838,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"; }; features = builtins.concatLists [ - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web") "default") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model") "default") [ "lazy_static" ] [ "std" ] ]; @@ -1538,32 +1538,6 @@ in }; }); - "registry+https://github.com/rust-lang/crates.io-index".garage_model."0.5.1" = overridableMkRustCrate (profileName: rec { - name = "garage_model"; - version = "0.5.1"; - registry = "registry+https://github.com/rust-lang/crates.io-index"; - src = fetchCratesIo { inherit name version; sha256 = "584619e8999713d73761775591ad6f01ff8c9d724f3b20984f5932f1fc7f9988"; }; - dependencies = { - arc_swap = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; - async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; - futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; - futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; - garage_rpc = rustPackages."registry+https://github.com/rust-lang/crates.io-index".garage_rpc."0.5.1" { inherit profileName; }; - garage_table = rustPackages."registry+https://github.com/rust-lang/crates.io-index".garage_table."0.5.1" { inherit profileName; }; - garage_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".garage_util."0.5.1" { inherit profileName; }; - hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; - log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; - netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.3.1" { inherit profileName; }; - rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; - rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; - serde_bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; - sled = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; - tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; - zstd = rustPackages."registry+https://github.com/rust-lang/crates.io-index".zstd."0.9.2+zstd.1.5.1" { inherit profileName; }; - }; - }); - "unknown".garage_model."0.8.0" = overridableMkRustCrate (profileName: rec { name = "garage_model"; version = "0.8.0"; @@ -1585,7 +1559,6 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_block" else null } = rustPackages."unknown".garage_block."0.8.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_db" else null } = rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_model_050" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".garage_model."0.5.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_rpc" else null } = rustPackages."unknown".garage_rpc."0.8.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_table" else null } = rustPackages."unknown".garage_table."0.8.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }; @@ -1602,34 +1575,6 @@ in }; }); - "registry+https://github.com/rust-lang/crates.io-index".garage_rpc."0.5.1" = overridableMkRustCrate (profileName: rec { - name = "garage_rpc"; - version = "0.5.1"; - registry = "registry+https://github.com/rust-lang/crates.io-index"; - src = fetchCratesIo { inherit name version; sha256 = "81e693aa4582cfe7a7ce70c07880e3662544b5d0cd68bc4b59c53febfbb8d1ec"; }; - dependencies = { - arc_swap = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; - async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; - futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; - futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; - garage_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".garage_util."0.5.1" { inherit profileName; }; - gethostname = rustPackages."registry+https://github.com/rust-lang/crates.io-index".gethostname."0.2.3" { inherit profileName; }; - hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; - hyper = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; - sodiumoxide = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; - log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; - netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.3.1" { inherit profileName; }; - rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; - rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; - serde_bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; - serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; - tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; - tokio_stream = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.8" { inherit profileName; }; - }; - }); - "unknown".garage_rpc."0.8.0" = overridableMkRustCrate (profileName: rec { name = "garage_rpc"; version = "0.8.0"; @@ -1672,29 +1617,6 @@ in }; }); - "registry+https://github.com/rust-lang/crates.io-index".garage_table."0.5.1" = overridableMkRustCrate (profileName: rec { - name = "garage_table"; - version = "0.5.1"; - registry = "registry+https://github.com/rust-lang/crates.io-index"; - src = fetchCratesIo { inherit name version; sha256 = "5c3557f3757e2acd29eaee86804d4e6c38d2abda81b4b349d8a0d2277044265c"; }; - dependencies = { - async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; }; - futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; - futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }; - garage_rpc = rustPackages."registry+https://github.com/rust-lang/crates.io-index".garage_rpc."0.5.1" { inherit profileName; }; - garage_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".garage_util."0.5.1" { inherit profileName; }; - hexdump = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hexdump."0.1.1" { inherit profileName; }; - log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; - rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; - rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; - serde_bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.5" { inherit profileName; }; - sled = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; - tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; - }; - }); - "unknown".garage_table."0.8.0" = overridableMkRustCrate (profileName: rec { name = "garage_table"; version = "0.8.0"; @@ -1719,33 +1641,6 @@ in }; }); - "registry+https://github.com/rust-lang/crates.io-index".garage_util."0.5.1" = overridableMkRustCrate (profileName: rec { - name = "garage_util"; - version = "0.5.1"; - registry = "registry+https://github.com/rust-lang/crates.io-index"; - src = fetchCratesIo { inherit name version; sha256 = "1e096994382447431e2f3c70e3685eb8b24c00eceff8667bb22a2a27ff17832f"; }; - dependencies = { - blake2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".blake2."0.9.2" { inherit profileName; }; - chrono = rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.19" { inherit profileName; }; - err_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; - futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; - hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; - http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; - hyper = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; - log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; - netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.3.1" { inherit profileName; }; - rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; - rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; - serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; - sha2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sha2."0.9.9" { inherit profileName; }; - sled = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }; - tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; - toml = rustPackages."registry+https://github.com/rust-lang/crates.io-index".toml."0.5.8" { inherit profileName; }; - xxhash_rust = rustPackages."registry+https://github.com/rust-lang/crates.io-index".xxhash-rust."0.8.4" { inherit profileName; }; - }; - }); - "unknown".garage_util."0.8.0" = overridableMkRustCrate (profileName: rec { name = "garage_util"; version = "0.8.0"; @@ -2860,32 +2755,6 @@ in }; }); - "registry+https://github.com/rust-lang/crates.io-index".netapp."0.3.1" = overridableMkRustCrate (profileName: rec { - name = "netapp"; - version = "0.3.1"; - registry = "registry+https://github.com/rust-lang/crates.io-index"; - src = fetchCratesIo { inherit name version; sha256 = "ac7dcd2c6c5a9d5ea88ffc17d3339d49d308e68c4d8190c494b55ddbf78d80ad"; }; - features = builtins.concatLists [ - [ "default" ] - ]; - dependencies = { - arc_swap = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; - async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."0.6.0" { inherit profileName; }; - err_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.2.4" { profileName = "__noProfile"; }; - futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; - hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; - kuska_handshake = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-handshake."0.2.0" { inherit profileName; }; - sodiumoxide = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; - log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; - rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.14.4" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; - tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; - tokio_stream = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.8" { inherit profileName; }; - tokio_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.6.9" { inherit profileName; }; - }; - }); - "registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" = overridableMkRustCrate (profileName: rec { name = "netapp"; version = "0.4.4"; @@ -5507,10 +5376,10 @@ in (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "namedpipeapi") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "ntdef") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "ntsecapi") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web") "ntstatus") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model") "ntstatus") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "objbase") [ "processenv" ] - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web") "processthreadsapi") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_db" || rootFeatures' ? "garage_model") "processthreadsapi") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "profileapi") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "schannel") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "k2v-client") "securitybaseapi") @@ -5580,10 +5449,10 @@ in ]; dependencies = { ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-uwp-windows-gnu" || hostPlatform.config == "i686-pc-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); diff --git a/src/model/Cargo.toml b/src/model/Cargo.toml index cb0017b2..bbcfe89c 100644 --- a/src/model/Cargo.toml +++ b/src/model/Cargo.toml @@ -19,7 +19,6 @@ garage_rpc = { version = "0.8.0", path = "../rpc" } garage_table = { version = "0.8.0", path = "../table" } garage_block = { version = "0.8.0", path = "../block" } garage_util = { version = "0.8.0", path = "../util" } -garage_model_050 = { package = "garage_model", version = "0.5.1" } async-trait = "0.1.7" arc-swap = "1.0" diff --git a/src/model/key_table.rs b/src/model/key_table.rs index 330e83f0..7288f6e4 100644 --- a/src/model/key_table.rs +++ b/src/model/key_table.rs @@ -6,7 +6,7 @@ use garage_util::data::*; use crate::permission::BucketKeyPerm; -use garage_model_050::key_table as old; +use crate::prev::v051::key_table as old; /// An api key #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] diff --git a/src/model/lib.rs b/src/model/lib.rs index 7c9d9270..4f20ea46 100644 --- a/src/model/lib.rs +++ b/src/model/lib.rs @@ -1,6 +1,9 @@ #[macro_use] extern crate tracing; +// For migration from previous versions +pub(crate) mod prev; + pub mod permission; pub mod index_counter; diff --git a/src/model/migrate.rs b/src/model/migrate.rs index 5fc67069..cd6ad26a 100644 --- a/src/model/migrate.rs +++ b/src/model/migrate.rs @@ -5,7 +5,7 @@ use garage_util::data::*; use garage_util::error::Error as GarageError; use garage_util::time::*; -use garage_model_050::bucket_table as old_bucket; +use crate::prev::v051::bucket_table as old_bucket; use crate::bucket_alias_table::*; use crate::bucket_table::*; diff --git a/src/model/prev/mod.rs b/src/model/prev/mod.rs new file mode 100644 index 00000000..68bb1502 --- /dev/null +++ b/src/model/prev/mod.rs @@ -0,0 +1 @@ +pub(crate) mod v051; diff --git a/src/model/prev/v051/bucket_table.rs b/src/model/prev/v051/bucket_table.rs new file mode 100644 index 00000000..0c52b6ea --- /dev/null +++ b/src/model/prev/v051/bucket_table.rs @@ -0,0 +1,63 @@ +use serde::{Deserialize, Serialize}; + +use garage_table::crdt::Crdt; +use garage_table::*; + +use super::key_table::PermissionSet; + +/// A bucket is a collection of objects +/// +/// Its parameters are not directly accessible as: +/// - It must be possible to merge paramaters, hence the use of a LWW CRDT. +/// - A bucket has 2 states, Present or Deleted and parameters make sense only if present. +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +pub struct Bucket { + /// Name of the bucket + pub name: String, + /// State, and configuration if not deleted, of the bucket + pub state: crdt::Lww, +} + +/// State of a bucket +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +pub enum BucketState { + /// The bucket is deleted + Deleted, + /// The bucket exists + Present(BucketParams), +} + +impl Crdt for BucketState { + fn merge(&mut self, o: &Self) { + match o { + BucketState::Deleted => *self = BucketState::Deleted, + BucketState::Present(other_params) => { + if let BucketState::Present(params) = self { + params.merge(other_params); + } + } + } + } +} + +/// Configuration for a bucket +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +pub struct BucketParams { + /// Map of key with access to the bucket, and what kind of access they give + pub authorized_keys: crdt::LwwMap, + /// Is the bucket served as http + pub website: crdt::Lww, +} + +impl Crdt for BucketParams { + fn merge(&mut self, o: &Self) { + self.authorized_keys.merge(&o.authorized_keys); + self.website.merge(&o.website); + } +} + +impl Crdt for Bucket { + fn merge(&mut self, other: &Self) { + self.state.merge(&other.state); + } +} diff --git a/src/model/prev/v051/key_table.rs b/src/model/prev/v051/key_table.rs new file mode 100644 index 00000000..dab6caa7 --- /dev/null +++ b/src/model/prev/v051/key_table.rs @@ -0,0 +1,51 @@ +use serde::{Deserialize, Serialize}; + +use garage_table::crdt::*; +use garage_table::*; + +/// An api key +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +pub struct Key { + /// The id of the key (immutable), used as partition key + pub key_id: String, + + /// The secret_key associated + pub secret_key: String, + + /// Name for the key + pub name: crdt::Lww, + + /// Is the key deleted + pub deleted: crdt::Bool, + + /// Buckets in which the key is authorized. Empty if `Key` is deleted + // CRDT interaction: deleted implies authorized_buckets is empty + pub authorized_buckets: crdt::LwwMap, +} + +/// Permission given to a key in a bucket +#[derive(PartialOrd, Ord, PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] +pub struct PermissionSet { + /// The key can be used to read the bucket + pub allow_read: bool, + /// The key can be used to write in the bucket + pub allow_write: bool, +} + +impl AutoCrdt for PermissionSet { + const WARN_IF_DIFFERENT: bool = true; +} + +impl Crdt for Key { + fn merge(&mut self, other: &Self) { + self.name.merge(&other.name); + self.deleted.merge(&other.deleted); + + if self.deleted.get() { + self.authorized_buckets.clear(); + } else { + self.authorized_buckets.merge(&other.authorized_buckets); + } + } +} + diff --git a/src/model/prev/v051/mod.rs b/src/model/prev/v051/mod.rs new file mode 100644 index 00000000..7a954752 --- /dev/null +++ b/src/model/prev/v051/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod bucket_table; +pub(crate) mod key_table; +pub(crate) mod object_table; +pub(crate) mod version_table; diff --git a/src/model/prev/v051/object_table.rs b/src/model/prev/v051/object_table.rs new file mode 100644 index 00000000..fe35d683 --- /dev/null +++ b/src/model/prev/v051/object_table.rs @@ -0,0 +1,150 @@ +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +use garage_util::data::*; + +use garage_table::crdt::*; + +/// An object +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +pub struct Object { + /// The bucket in which the object is stored, used as partition key + pub bucket: String, + + /// The key at which the object is stored in its bucket, used as sorting key + pub key: String, + + /// The list of currenty stored versions of the object + versions: Vec, +} + +impl Object { + /// Get a list of currently stored versions of `Object` + pub fn versions(&self) -> &[ObjectVersion] { + &self.versions[..] + } +} + +/// Informations about a version of an object +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +pub struct ObjectVersion { + /// Id of the version + pub uuid: Uuid, + /// Timestamp of when the object was created + pub timestamp: u64, + /// State of the version + pub state: ObjectVersionState, +} + +/// State of an object version +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +pub enum ObjectVersionState { + /// The version is being received + Uploading(ObjectVersionHeaders), + /// The version is fully received + Complete(ObjectVersionData), + /// The version uploaded containded errors or the upload was explicitly aborted + Aborted, +} + +impl Crdt for ObjectVersionState { + fn merge(&mut self, other: &Self) { + use ObjectVersionState::*; + match other { + Aborted => { + *self = Aborted; + } + Complete(b) => match self { + Aborted => {} + Complete(a) => { + a.merge(b); + } + Uploading(_) => { + *self = Complete(b.clone()); + } + }, + Uploading(_) => {} + } + } +} + +/// Data stored in object version +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Serialize, Deserialize)] +pub enum ObjectVersionData { + /// The object was deleted, this Version is a tombstone to mark it as such + DeleteMarker, + /// The object is short, it's stored inlined + Inline(ObjectVersionMeta, #[serde(with = "serde_bytes")] Vec), + /// The object is not short, Hash of first block is stored here, next segments hashes are + /// stored in the version table + FirstBlock(ObjectVersionMeta, Hash), +} + +impl AutoCrdt for ObjectVersionData { + const WARN_IF_DIFFERENT: bool = true; +} + +/// Metadata about the object version +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Serialize, Deserialize)] +pub struct ObjectVersionMeta { + /// Headers to send to the client + pub headers: ObjectVersionHeaders, + /// Size of the object + pub size: u64, + /// etag of the object + pub etag: String, +} + +/// Additional headers for an object +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Serialize, Deserialize)] +pub struct ObjectVersionHeaders { + /// Content type of the object + pub content_type: String, + /// Any other http headers to send + pub other: BTreeMap, +} + +impl ObjectVersion { + fn cmp_key(&self) -> (u64, Uuid) { + (self.timestamp, self.uuid) + } + + /// Is the object version completely received + pub fn is_complete(&self) -> bool { + matches!(self.state, ObjectVersionState::Complete(_)) + } +} + +impl Crdt for Object { + fn merge(&mut self, other: &Self) { + // Merge versions from other into here + for other_v in other.versions.iter() { + match self + .versions + .binary_search_by(|v| v.cmp_key().cmp(&other_v.cmp_key())) + { + Ok(i) => { + self.versions[i].state.merge(&other_v.state); + } + Err(i) => { + self.versions.insert(i, other_v.clone()); + } + } + } + + // Remove versions which are obsolete, i.e. those that come + // before the last version which .is_complete(). + let last_complete = self + .versions + .iter() + .enumerate() + .rev() + .find(|(_, v)| v.is_complete()) + .map(|(vi, _)| vi); + + if let Some(last_vi) = last_complete { + self.versions = self.versions.drain(last_vi..).collect::>(); + } + } +} + diff --git a/src/model/prev/v051/version_table.rs b/src/model/prev/v051/version_table.rs new file mode 100644 index 00000000..1e658f91 --- /dev/null +++ b/src/model/prev/v051/version_table.rs @@ -0,0 +1,79 @@ +use serde::{Deserialize, Serialize}; + +use garage_util::data::*; + +use garage_table::crdt::*; +use garage_table::*; + +/// A version of an object +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +pub struct Version { + /// UUID of the version, used as partition key + pub uuid: Uuid, + + // Actual data: the blocks for this version + // In the case of a multipart upload, also store the etags + // of individual parts and check them when doing CompleteMultipartUpload + /// Is this version deleted + pub deleted: crdt::Bool, + /// list of blocks of data composing the version + pub blocks: crdt::Map, + /// Etag of each part in case of a multipart upload, empty otherwise + pub parts_etags: crdt::Map, + + // Back link to bucket+key so that we can figure if + // this was deleted later on + /// Bucket in which the related object is stored + pub bucket: String, + /// Key in which the related object is stored + pub key: String, +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)] +pub struct VersionBlockKey { + /// Number of the part + pub part_number: u64, + /// Offset of this sub-segment in its part + pub offset: u64, +} + +impl Ord for VersionBlockKey { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.part_number + .cmp(&other.part_number) + .then(self.offset.cmp(&other.offset)) + } +} + +impl PartialOrd for VersionBlockKey { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// Informations about a single block +#[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Copy, Debug, Serialize, Deserialize)] +pub struct VersionBlock { + /// Blake2 sum of the block + pub hash: Hash, + /// Size of the block + pub size: u64, +} + +impl AutoCrdt for VersionBlock { + const WARN_IF_DIFFERENT: bool = true; +} + +impl Crdt for Version { + fn merge(&mut self, other: &Self) { + self.deleted.merge(&other.deleted); + + if self.deleted.get() { + self.blocks.clear(); + self.parts_etags.clear(); + } else { + self.blocks.merge(&other.blocks); + self.parts_etags.merge(&other.parts_etags); + } + } +} diff --git a/src/model/s3/object_table.rs b/src/model/s3/object_table.rs index a3914c36..a151f1b1 100644 --- a/src/model/s3/object_table.rs +++ b/src/model/s3/object_table.rs @@ -14,7 +14,7 @@ use garage_table::*; use crate::index_counter::*; use crate::s3::version_table::*; -use garage_model_050::object_table as old; +use crate::prev::v051::object_table as old; pub const OBJECTS: &str = "objects"; pub const UNFINISHED_UPLOADS: &str = "unfinished_uploads"; diff --git a/src/model/s3/version_table.rs b/src/model/s3/version_table.rs index 881c245a..b545e66a 100644 --- a/src/model/s3/version_table.rs +++ b/src/model/s3/version_table.rs @@ -12,7 +12,7 @@ use garage_table::*; use crate::s3::block_ref_table::*; -use garage_model_050::version_table as old; +use crate::prev::v051::version_table as old; /// A version of an object #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] From 6f02c36a89d93b04944a3f0882b6f6b703d9c012 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 6 Sep 2022 17:59:41 +0200 Subject: [PATCH 101/149] cargo fmt --- src/model/prev/v051/key_table.rs | 1 - src/model/prev/v051/object_table.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/model/prev/v051/key_table.rs b/src/model/prev/v051/key_table.rs index dab6caa7..fee24741 100644 --- a/src/model/prev/v051/key_table.rs +++ b/src/model/prev/v051/key_table.rs @@ -48,4 +48,3 @@ impl Crdt for Key { } } } - diff --git a/src/model/prev/v051/object_table.rs b/src/model/prev/v051/object_table.rs index fe35d683..cb59b309 100644 --- a/src/model/prev/v051/object_table.rs +++ b/src/model/prev/v051/object_table.rs @@ -147,4 +147,3 @@ impl Crdt for Object { } } } - From c2cc08852bcbd94bad5c15c39e7145c0496d7241 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 6 Sep 2022 19:31:42 +0200 Subject: [PATCH 102/149] Reenable node ordering --- src/block/manager.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/block/manager.rs b/src/block/manager.rs index a9def3b0..66a454b0 100644 --- a/src/block/manager.rs +++ b/src/block/manager.rs @@ -9,7 +9,7 @@ use async_trait::async_trait; use bytes::Bytes; use serde::{Deserialize, Serialize}; -use futures::{Stream, TryStreamExt}; +use futures::Stream; use futures_util::stream::StreamExt; use tokio::fs; use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader}; @@ -191,7 +191,7 @@ impl BlockManager { order_tag: Option, ) -> Result<(DataBlockHeader, ByteStream), Error> { let who = self.replication.read_nodes(hash); - //let who = self.system.rpc.request_order(&who); + let who = self.system.rpc.request_order(&who); for node in who.iter() { let node_id = NodeID::from(*node); @@ -238,7 +238,7 @@ impl BlockManager { order_tag: Option, ) -> Result { let who = self.replication.read_nodes(hash); - //let who = self.system.rpc.request_order(&who); + let who = self.system.rpc.request_order(&who); for node in who.iter() { let node_id = NodeID::from(*node); @@ -296,9 +296,7 @@ impl BlockManager { > { let (header, stream) = self.rpc_get_raw_block_streaming(hash, order_tag).await?; match header { - DataBlockHeader::Plain => Ok(Box::pin(stream.map_err(|_| { - std::io::Error::new(std::io::ErrorKind::Other, "netapp stream error") - }))), + DataBlockHeader::Plain => Ok(Box::pin(stream)), DataBlockHeader::Compressed => { // Too many things, I hate it. let reader = stream_asyncread(stream); From 4024822585783368993ac26807d076d8c312bb35 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 6 Sep 2022 19:45:00 +0200 Subject: [PATCH 103/149] Update netapp to lastest git version with LAS scheduling --- Cargo.lock | 24 +++++------------- Cargo.nix | 73 +++++++++++++++++++----------------------------------- 2 files changed, 32 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 632c2131..6716ea12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2176,13 +2176,13 @@ dependencies = [ [[package]] name = "netapp" version = "0.5.0" -source = "git+https://git.deuxfleurs.fr/lx/netapp?branch=stream-body#f6ad1d0fab340e77fbfcb3488a98c342d334838e" +source = "git+https://git.deuxfleurs.fr/lx/netapp?branch=stream-body#0f799a7768997c37e3e1b6861c097c4cd934acde" dependencies = [ "arc-swap", "async-trait", "bytes 1.2.0", "cfg-if 1.0.0", - "err-derive 0.2.4", + "err-derive 0.3.1", "futures", "hex", "kuska-handshake", @@ -2191,12 +2191,12 @@ dependencies = [ "opentelemetry", "opentelemetry-contrib", "pin-project 1.0.10", - "rand 0.5.6", - "rmp-serde 0.14.4", + "rand 0.8.5", + "rmp-serde 0.15.5", "serde", "tokio", "tokio-stream", - "tokio-util 0.6.9", + "tokio-util 0.7.0", ] [[package]] @@ -2752,19 +2752,6 @@ dependencies = [ "proc-macro2", ] -[[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.6.5" @@ -3746,6 +3733,7 @@ checksum = "64910e1b9c1901aaf5375561e35b9c057d95ff41a44ede043a03e09279eabaf1" dependencies = [ "bytes 1.2.0", "futures-core", + "futures-io", "futures-sink", "log", "pin-project-lite", diff --git a/Cargo.nix b/Cargo.nix index 9f477491..9877f729 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -791,7 +791,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.config == "aarch64-linux-android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -2852,7 +2852,7 @@ in [ "os-poll" ] ]; dependencies = { - ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "wasi" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "wasi" || hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if hostPlatform.isWindows then "miow" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".miow."0.3.7" { inherit profileName; }; ${ if hostPlatform.isWindows then "ntapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ntapi."0.3.7" { inherit profileName; }; @@ -2965,7 +2965,7 @@ in url = https://git.deuxfleurs.fr/lx/netapp; name = "netapp"; version = "0.5.0"; - rev = "f6ad1d0fab340e77fbfcb3488a98c342d334838e"; + rev = "0f799a7768997c37e3e1b6861c097c4cd934acde"; ref = "stream-body";}; features = builtins.concatLists [ [ "default" ] @@ -2978,7 +2978,7 @@ in async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; cfg_if = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }; - err_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.2.4" { profileName = "__noProfile"; }; + err_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; kuska_handshake = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-handshake."0.2.0" { inherit profileName; }; @@ -2987,12 +2987,12 @@ in opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; opentelemetry_contrib = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-contrib."0.9.0" { inherit profileName; }; pin_project = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; }; - rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.5.6" { inherit profileName; }; - rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.14.4" { inherit profileName; }; + rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; + rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; tokio_stream = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.8" { inherit profileName; }; - tokio_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.6.9" { inherit profileName; }; + tokio_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.0" { inherit profileName; }; }; }); @@ -3718,29 +3718,6 @@ in }; }); - "registry+https://github.com/rust-lang/crates.io-index".rand."0.5.6" = overridableMkRustCrate (profileName: rec { - name = "rand"; - version = "0.5.6"; - registry = "registry+https://github.com/rust-lang/crates.io-index"; - src = fetchCratesIo { inherit name version; sha256 = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9"; }; - features = builtins.concatLists [ - [ "alloc" ] - [ "cloudabi" ] - [ "default" ] - [ "fuchsia-cprng" ] - [ "libc" ] - [ "std" ] - [ "winapi" ] - ]; - dependencies = { - ${ if hostPlatform.parsed.kernel.name == "cloudabi" then "cloudabi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cloudabi."0.0.3" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "fuchsia" then "fuchsia_cprng" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".fuchsia-cprng."0.1.1" { inherit profileName; }; - ${ if hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - rand_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_core."0.3.1" { inherit profileName; }; - ${ if hostPlatform.isWindows then "winapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".winapi."0.3.9" { inherit profileName; }; - }; - }); - "registry+https://github.com/rust-lang/crates.io-index".rand."0.6.5" = overridableMkRustCrate (profileName: rec { name = "rand"; version = "0.6.5"; @@ -3823,10 +3800,6 @@ in version = "0.3.1"; registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"; }; - features = builtins.concatLists [ - [ "alloc" ] - [ "std" ] - ]; dependencies = { rand_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand_core."0.4.2" { inherit profileName; }; }; @@ -4676,7 +4649,7 @@ in ]; dependencies = { bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot_core."0.8.5" { inherit profileName; }; static_init_macro = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init_macro."1.0.2" { profileName = "__noProfile"; }; @@ -5054,22 +5027,22 @@ in src = fetchCratesIo { inherit name version; sha256 = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0"; }; features = builtins.concatLists [ (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "codec") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "compat") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web") "compat") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "futures-io") - (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "io") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web") "futures-io") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "io") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "slab") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "time") ]; dependencies = { - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_io" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-io."0.3.21" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_sink" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-sink."0.3.21" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "log" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "pin_project_lite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "futures_io" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-io."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures_sink" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-sink."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "log" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "pin_project_lite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "slab" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".slab."0.4.5" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; }; }); @@ -5078,9 +5051,15 @@ in version = "0.7.0"; registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "64910e1b9c1901aaf5375561e35b9c057d95ff41a44ede043a03e09279eabaf1"; }; + features = builtins.concatLists [ + [ "compat" ] + [ "futures-io" ] + [ "io" ] + ]; dependencies = { bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; futures_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; }; + futures_io = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-io."0.3.21" { inherit profileName; }; futures_sink = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-sink."0.3.21" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; }; @@ -5774,9 +5753,9 @@ in ]; dependencies = { ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-uwp-windows-gnu" || hostPlatform.config == "i686-pc-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); From 907054775dc71a10a92ab96112889db9113130ab Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 6 Sep 2022 22:25:23 +0200 Subject: [PATCH 104/149] Faster copy, better get error message --- src/api/s3/copy.rs | 12 +++++------- src/api/s3/get.rs | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/api/s3/copy.rs b/src/api/s3/copy.rs index 10cf5935..a1a8c9a4 100644 --- a/src/api/s3/copy.rs +++ b/src/api/s3/copy.rs @@ -9,6 +9,7 @@ use bytes::Bytes; use hyper::{Body, Request, Response}; use serde::Serialize; +use garage_rpc::netapp::bytes_buf::BytesBuf; use garage_rpc::rpc_helper::OrderTag; use garage_table::*; use garage_util::data::*; @@ -566,7 +567,7 @@ type BlockStreamItem = Result; struct Defragmenter> { block_size: usize, block_stream: Pin>>, - buffer: Vec, + buffer: BytesBuf, hash: Option, } @@ -575,7 +576,7 @@ impl> Defragmenter { Self { block_size, block_stream, - buffer: vec![], + buffer: BytesBuf::new(), hash: None, } } @@ -593,7 +594,7 @@ impl> Defragmenter { if self.buffer.is_empty() { let (next_block, next_block_hash) = self.block_stream.next().await.unwrap()?; - self.buffer = next_block.to_vec(); // TODO TOO MUCH COPY + self.buffer.extend(next_block); self.hash = next_block_hash; } else if self.buffer.len() + peeked_next_block.len() > self.block_size { break; @@ -604,10 +605,7 @@ impl> Defragmenter { } } - Ok(( - Bytes::from(std::mem::take(&mut self.buffer)), - self.hash.take(), - )) + Ok((self.buffer.take_all(), self.hash.take())) } } diff --git a/src/api/s3/get.rs b/src/api/s3/get.rs index dfc284fe..dd95f6e7 100644 --- a/src/api/s3/get.rs +++ b/src/api/s3/get.rs @@ -274,11 +274,11 @@ pub async fn handle_get( .block_manager .rpc_get_block_streaming(&hash, Some(order_stream.order(i as u64))) .await - .unwrap_or_else(|_| { + .unwrap_or_else(|e| { Box::pin(futures::stream::once(async move { Err(std::io::Error::new( std::io::ErrorKind::Other, - "Could not get next block", + format!("Could not get block {}: {}", i, e), )) })) }) From db61f41030678c5756c844c8aa41a210c658769e Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 7 Sep 2022 11:59:56 +0200 Subject: [PATCH 105/149] Move GIT_VERSION injection later in build chain to reduce build times --- Cargo.lock | 2 +- Cargo.nix | 4 +- nix/compile.nix | 20 +++---- src/api/admin/cluster.rs | 2 +- src/garage/admin.rs | 2 +- src/garage/cli/structs.rs | 104 ++++++++++++++++----------------- src/garage/main.rs | 4 +- src/model/Cargo.toml | 1 + src/model/lib.rs | 1 + src/{util => model}/version.rs | 2 +- src/rpc/system.rs | 11 ++-- src/util/Cargo.toml | 1 - src/util/lib.rs | 1 - 13 files changed, 76 insertions(+), 79 deletions(-) rename src/{util => model}/version.rs (76%) diff --git a/Cargo.lock b/Cargo.lock index 976acfc8..a72b92c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1099,6 +1099,7 @@ dependencies = [ "garage_rpc", "garage_table", "garage_util", + "git-version", "hex", "netapp", "opentelemetry", @@ -1173,7 +1174,6 @@ dependencies = [ "err-derive 0.3.1", "futures", "garage_db", - "git-version", "hex", "http", "hyper", diff --git a/Cargo.nix b/Cargo.nix index 614050c8..8ec31508 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -1562,6 +1562,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_rpc" else null } = rustPackages."unknown".garage_rpc."0.8.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_table" else null } = rustPackages."unknown".garage_table."0.8.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "git_version" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".git-version."0.3.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; @@ -1656,7 +1657,6 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "err_derive" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "garage_db" else null } = rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "git_version" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".git-version."0.3.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "http" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "hyper" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; @@ -4428,7 +4428,7 @@ in ]; dependencies = { bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot_core."0.8.5" { inherit profileName; }; static_init_macro = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init_macro."1.0.2" { profileName = "__noProfile"; }; diff --git a/nix/compile.nix b/nix/compile.nix index ea431a7e..d24cd917 100644 --- a/nix/compile.nix +++ b/nix/compile.nix @@ -146,15 +146,7 @@ let (pkgs.rustBuilder.rustLib.makeOverride { name = "garage_util"; - overrideAttrs = drv: - (if git_version != null then { - /* [3] */ preConfigure = '' - ${drv.preConfigure or ""} - export GIT_VERSION="${git_version}" - ''; - } else {}) - // - { /* [1] */ setBuildEnv = (buildEnv drv); }; + overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; }) (pkgs.rustBuilder.rustLib.makeOverride { @@ -169,7 +161,15 @@ let (pkgs.rustBuilder.rustLib.makeOverride { name = "garage_model"; - overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; + overrideAttrs = drv: + (if git_version != null then { + /* [3] */ preConfigure = '' + ${drv.preConfigure or ""} + export GIT_VERSION="${git_version}" + ''; + } else {}) + // + { /* [1] */ setBuildEnv = (buildEnv drv); }; }) (pkgs.rustBuilder.rustLib.makeOverride { diff --git a/src/api/admin/cluster.rs b/src/api/admin/cluster.rs index 4b7716a3..8e6dfb3f 100644 --- a/src/api/admin/cluster.rs +++ b/src/api/admin/cluster.rs @@ -18,7 +18,7 @@ use crate::helpers::{json_ok_response, parse_json_body}; pub async fn handle_get_cluster_status(garage: &Arc) -> Result, Error> { let res = GetClusterStatusResponse { node: hex::encode(garage.system.id), - garage_version: garage.system.garage_version(), + garage_version: garage_model::version::garage_version(), db_engine: garage.db.engine(), known_nodes: garage .system diff --git a/src/garage/admin.rs b/src/garage/admin.rs index 71ee608c..f4c182fe 100644 --- a/src/garage/admin.rs +++ b/src/garage/admin.rs @@ -740,7 +740,7 @@ impl AdminRpcHandler { writeln!( &mut ret, "\nGarage version: {}", - self.garage.system.garage_version(), + garage_model::version::garage_version(), ) .unwrap(); writeln!(&mut ret, "\nDatabase engine: {}", self.garage.db.engine()).unwrap(); diff --git a/src/garage/cli/structs.rs b/src/garage/cli/structs.rs index 9274f80f..018c8119 100644 --- a/src/garage/cli/structs.rs +++ b/src/garage/cli/structs.rs @@ -1,65 +1,65 @@ use serde::{Deserialize, Serialize}; - -use garage_util::version; use structopt::StructOpt; +use garage_model::version::garage_version; + #[derive(StructOpt, Debug)] pub enum Command { /// Run Garage server - #[structopt(name = "server", version = version::garage())] + #[structopt(name = "server", version = garage_version())] Server, /// Get network status - #[structopt(name = "status", version = version::garage())] + #[structopt(name = "status", version = garage_version())] Status, /// Operations on individual Garage nodes - #[structopt(name = "node", version = version::garage())] + #[structopt(name = "node", version = garage_version())] Node(NodeOperation), /// Operations on the assignation of node roles in the cluster layout - #[structopt(name = "layout", version = version::garage())] + #[structopt(name = "layout", version = garage_version())] Layout(LayoutOperation), /// Operations on buckets - #[structopt(name = "bucket", version = version::garage())] + #[structopt(name = "bucket", version = garage_version())] Bucket(BucketOperation), /// Operations on S3 access keys - #[structopt(name = "key", version = version::garage())] + #[structopt(name = "key", version = garage_version())] Key(KeyOperation), /// Run migrations from previous Garage version /// (DO NOT USE WITHOUT READING FULL DOCUMENTATION) - #[structopt(name = "migrate", version = version::garage())] + #[structopt(name = "migrate", version = garage_version())] Migrate(MigrateOpt), /// Start repair of node data on remote node - #[structopt(name = "repair", version = version::garage())] + #[structopt(name = "repair", version = garage_version())] Repair(RepairOpt), /// Offline reparation of node data (these repairs must be run offline /// directly on the server node) - #[structopt(name = "offline-repair", version = version::garage())] + #[structopt(name = "offline-repair", version = garage_version())] OfflineRepair(OfflineRepairOpt), /// Gather node statistics - #[structopt(name = "stats", version = version::garage())] + #[structopt(name = "stats", version = garage_version())] Stats(StatsOpt), /// Manage background workers - #[structopt(name = "worker", version = version::garage())] + #[structopt(name = "worker", version = garage_version())] Worker(WorkerOpt), } #[derive(StructOpt, Debug)] pub enum NodeOperation { /// Print identifier (public key) of this Garage node - #[structopt(name = "id", version = version::garage())] + #[structopt(name = "id", version = garage_version())] NodeId(NodeIdOpt), /// Connect to Garage node that is currently isolated from the system - #[structopt(name = "connect", version = version::garage())] + #[structopt(name = "connect", version = garage_version())] Connect(ConnectNodeOpt), } @@ -80,23 +80,23 @@ pub struct ConnectNodeOpt { #[derive(StructOpt, Debug)] pub enum LayoutOperation { /// Assign role to Garage node - #[structopt(name = "assign", version = version::garage())] + #[structopt(name = "assign", version = garage_version())] Assign(AssignRoleOpt), /// Remove role from Garage cluster node - #[structopt(name = "remove", version = version::garage())] + #[structopt(name = "remove", version = garage_version())] Remove(RemoveRoleOpt), /// Show roles currently assigned to nodes and changes staged for commit - #[structopt(name = "show", version = version::garage())] + #[structopt(name = "show", version = garage_version())] Show, /// Apply staged changes to cluster layout - #[structopt(name = "apply", version = version::garage())] + #[structopt(name = "apply", version = garage_version())] Apply(ApplyLayoutOpt), /// Revert staged changes to cluster layout - #[structopt(name = "revert", version = version::garage())] + #[structopt(name = "revert", version = garage_version())] Revert(RevertLayoutOpt), } @@ -151,43 +151,43 @@ pub struct RevertLayoutOpt { #[derive(Serialize, Deserialize, StructOpt, Debug)] pub enum BucketOperation { /// List buckets - #[structopt(name = "list", version = version::garage())] + #[structopt(name = "list", version = garage_version())] List, /// Get bucket info - #[structopt(name = "info", version = version::garage())] + #[structopt(name = "info", version = garage_version())] Info(BucketOpt), /// Create bucket - #[structopt(name = "create", version = version::garage())] + #[structopt(name = "create", version = garage_version())] Create(BucketOpt), /// Delete bucket - #[structopt(name = "delete", version = version::garage())] + #[structopt(name = "delete", version = garage_version())] Delete(DeleteBucketOpt), /// Alias bucket under new name - #[structopt(name = "alias", version = version::garage())] + #[structopt(name = "alias", version = garage_version())] Alias(AliasBucketOpt), /// Remove bucket alias - #[structopt(name = "unalias", version = version::garage())] + #[structopt(name = "unalias", version = garage_version())] Unalias(UnaliasBucketOpt), /// Allow key to read or write to bucket - #[structopt(name = "allow", version = version::garage())] + #[structopt(name = "allow", version = garage_version())] Allow(PermBucketOpt), /// Deny key from reading or writing to bucket - #[structopt(name = "deny", version = version::garage())] + #[structopt(name = "deny", version = garage_version())] Deny(PermBucketOpt), /// Expose as website or not - #[structopt(name = "website", version = version::garage())] + #[structopt(name = "website", version = garage_version())] Website(WebsiteOpt), /// Set the quotas for this bucket - #[structopt(name = "set-quotas", version = version::garage())] + #[structopt(name = "set-quotas", version = garage_version())] SetQuotas(SetQuotasOpt), } @@ -293,35 +293,35 @@ pub struct SetQuotasOpt { #[derive(Serialize, Deserialize, StructOpt, Debug)] pub enum KeyOperation { /// List keys - #[structopt(name = "list", version = version::garage())] + #[structopt(name = "list", version = garage_version())] List, /// Get key info - #[structopt(name = "info", version = version::garage())] + #[structopt(name = "info", version = garage_version())] Info(KeyOpt), /// Create new key - #[structopt(name = "new", version = version::garage())] + #[structopt(name = "new", version = garage_version())] New(KeyNewOpt), /// Rename key - #[structopt(name = "rename", version = version::garage())] + #[structopt(name = "rename", version = garage_version())] Rename(KeyRenameOpt), /// Delete key - #[structopt(name = "delete", version = version::garage())] + #[structopt(name = "delete", version = garage_version())] Delete(KeyDeleteOpt), /// Set permission flags for key - #[structopt(name = "allow", version = version::garage())] + #[structopt(name = "allow", version = garage_version())] Allow(KeyPermOpt), /// Unset permission flags for key - #[structopt(name = "deny", version = version::garage())] + #[structopt(name = "deny", version = garage_version())] Deny(KeyPermOpt), /// Import key - #[structopt(name = "import", version = version::garage())] + #[structopt(name = "import", version = garage_version())] Import(KeyImportOpt), } @@ -393,7 +393,7 @@ pub struct MigrateOpt { #[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)] pub enum MigrateWhat { /// Migrate buckets and permissions from v0.5.0 - #[structopt(name = "buckets050", version = version::garage())] + #[structopt(name = "buckets050", version = garage_version())] Buckets050, } @@ -414,19 +414,19 @@ pub struct RepairOpt { #[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)] pub enum RepairWhat { /// Only do a full sync of metadata tables - #[structopt(name = "tables", version = version::garage())] + #[structopt(name = "tables", version = garage_version())] Tables, /// Only repair (resync/rebalance) the set of stored blocks - #[structopt(name = "blocks", version = version::garage())] + #[structopt(name = "blocks", version = garage_version())] Blocks, /// Only redo the propagation of object deletions to the version table (slow) - #[structopt(name = "versions", version = version::garage())] + #[structopt(name = "versions", version = garage_version())] Versions, /// Only redo the propagation of version deletions to the block ref table (extremely slow) - #[structopt(name = "block_refs", version = version::garage())] + #[structopt(name = "block_refs", version = garage_version())] BlockRefs, /// Verify integrity of all blocks on disc (extremely slow, i/o intensive) - #[structopt(name = "scrub", version = version::garage())] + #[structopt(name = "scrub", version = garage_version())] Scrub { #[structopt(subcommand)] cmd: ScrubCmd, @@ -436,19 +436,19 @@ pub enum RepairWhat { #[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)] pub enum ScrubCmd { /// Start scrub - #[structopt(name = "start", version = version::garage())] + #[structopt(name = "start", version = garage_version())] Start, /// Pause scrub (it will resume automatically after 24 hours) - #[structopt(name = "pause", version = version::garage())] + #[structopt(name = "pause", version = garage_version())] Pause, /// Resume paused scrub - #[structopt(name = "resume", version = version::garage())] + #[structopt(name = "resume", version = garage_version())] Resume, /// Cancel scrub in progress - #[structopt(name = "cancel", version = version::garage())] + #[structopt(name = "cancel", version = garage_version())] Cancel, /// Set tranquility level for in-progress and future scrubs - #[structopt(name = "set-tranquility", version = version::garage())] + #[structopt(name = "set-tranquility", version = garage_version())] SetTranquility { #[structopt()] tranquility: u32, @@ -469,10 +469,10 @@ pub struct OfflineRepairOpt { pub enum OfflineRepairWhat { /// Repair K2V item counters #[cfg(feature = "k2v")] - #[structopt(name = "k2v_item_counters", version = version::garage())] + #[structopt(name = "k2v_item_counters", version = garage_version())] K2VItemCounters, /// Repair object counters - #[structopt(name = "object_counters", version = version::garage())] + #[structopt(name = "object_counters", version = garage_version())] ObjectCounters, } @@ -496,7 +496,7 @@ pub struct WorkerOpt { #[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)] pub enum WorkerCmd { /// List all workers on Garage node - #[structopt(name = "list", version = version::garage())] + #[structopt(name = "list", version = garage_version())] List { #[structopt(flatten)] opt: WorkerListOpt, diff --git a/src/garage/main.rs b/src/garage/main.rs index 8f0b377e..94c9bf61 100644 --- a/src/garage/main.rs +++ b/src/garage/main.rs @@ -23,15 +23,15 @@ use garage_util::error::*; use garage_rpc::system::*; use garage_rpc::*; -use garage_util::version; use garage_model::helper::error::Error as HelperError; +use garage_model::version::garage_version; use admin::*; use cli::*; #[derive(StructOpt, Debug)] -#[structopt(name = "garage", version = version::garage(), about = "S3-compatible object store for self-hosted geo-distributed deployments")] +#[structopt(name = "garage", version = garage_version(), about = "S3-compatible object store for self-hosted geo-distributed deployments")] struct Opt { /// Host to connect to for admin operations, in the format: /// @: diff --git a/src/model/Cargo.toml b/src/model/Cargo.toml index bbcfe89c..c41d3f16 100644 --- a/src/model/Cargo.toml +++ b/src/model/Cargo.toml @@ -24,6 +24,7 @@ async-trait = "0.1.7" arc-swap = "1.0" blake2 = "0.9" err-derive = "0.3" +git-version = "0.3.4" hex = "0.4" base64 = "0.13" tracing = "0.1.30" diff --git a/src/model/lib.rs b/src/model/lib.rs index 4f20ea46..43db01c5 100644 --- a/src/model/lib.rs +++ b/src/model/lib.rs @@ -19,3 +19,4 @@ pub mod s3; pub mod garage; pub mod helper; pub mod migrate; +pub mod version; diff --git a/src/util/version.rs b/src/model/version.rs similarity index 76% rename from src/util/version.rs rename to src/model/version.rs index 8882d035..cdb3ea62 100644 --- a/src/util/version.rs +++ b/src/model/version.rs @@ -1,4 +1,4 @@ -pub fn garage() -> &'static str { +pub fn garage_version() -> &'static str { option_env!("GIT_VERSION").unwrap_or(git_version::git_version!( prefix = "git:", cargo_prefix = "cargo:", diff --git a/src/rpc/system.rs b/src/rpc/system.rs index fbfbbf56..d621f59f 100644 --- a/src/rpc/system.rs +++ b/src/rpc/system.rs @@ -27,7 +27,6 @@ use garage_util::data::*; use garage_util::error::*; use garage_util::persister::Persister; use garage_util::time::*; -use garage_util::version; use crate::consul::*; #[cfg(feature = "kubernetes-discovery")] @@ -40,8 +39,10 @@ const DISCOVERY_INTERVAL: Duration = Duration::from_secs(60); const STATUS_EXCHANGE_INTERVAL: Duration = Duration::from_secs(10); const PING_TIMEOUT: Duration = Duration::from_secs(2); -/// Version tag used for version check upon Netapp connection -pub const GARAGE_VERSION_TAG: u64 = 0x6761726167650007; // garage 0x0007 +/// Version tag used for version check upon Netapp connection. +/// Cluster nodes with different version tags are deemed +/// incompatible and will refuse to connect. +pub const GARAGE_VERSION_TAG: u64 = 0x6761726167650008; // garage 0x0008 /// RPC endpoint used for calls related to membership pub const SYSTEM_RPC_PATH: &str = "garage_rpc/membership.rs/SystemRpc"; @@ -320,10 +321,6 @@ impl System { // ---- Administrative operations (directly available and // also available through RPC) ---- - pub fn garage_version(&self) -> &'static str { - version::garage() - } - pub fn get_known_nodes(&self) -> Vec { let node_status = self.node_status.read().unwrap(); let known_nodes = self diff --git a/src/util/Cargo.toml b/src/util/Cargo.toml index 5f3e5c57..af57008e 100644 --- a/src/util/Cargo.toml +++ b/src/util/Cargo.toml @@ -24,7 +24,6 @@ hex = "0.4" tracing = "0.1.30" rand = "0.8" sha2 = "0.9" -git-version = "0.3.4" chrono = "0.4" rmp-serde = "0.15" diff --git a/src/util/lib.rs b/src/util/lib.rs index 47c85c3a..fce151af 100644 --- a/src/util/lib.rs +++ b/src/util/lib.rs @@ -14,4 +14,3 @@ pub mod persister; pub mod time; pub mod token_bucket; pub mod tranquilizer; -pub mod version; From 28d86e76021bed674ca78684b9522cfb664a8ae2 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 7 Sep 2022 17:05:21 +0200 Subject: [PATCH 106/149] Report build features in garage --help --- Cargo.lock | 1 + Cargo.nix | 11 ++++++----- src/api/admin/cluster.rs | 2 ++ src/garage/admin.rs | 5 ++++- src/garage/main.rs | 36 +++++++++++++++++++++++++++++++++--- src/model/Cargo.toml | 1 + src/model/version.rs | 16 ++++++++++++++++ 7 files changed, 63 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a72b92c0..eaa8f1b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1101,6 +1101,7 @@ dependencies = [ "garage_util", "git-version", "hex", + "lazy_static", "netapp", "opentelemetry", "rand 0.8.5", diff --git a/Cargo.nix b/Cargo.nix index 8ec31508..6d60a041 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -749,7 +749,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.config == "aarch64-linux-android" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -1564,6 +1564,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "git_version" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".git-version."0.3.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "lazy_static" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; @@ -2676,7 +2677,7 @@ in [ "os-poll" ] ]; dependencies = { - ${ if hostPlatform.parsed.kernel.name == "wasi" || hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "wasi" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if hostPlatform.isWindows then "miow" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".miow."0.3.7" { inherit profileName; }; ${ if hostPlatform.isWindows then "ntapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ntapi."0.3.7" { inherit profileName; }; @@ -3832,7 +3833,7 @@ in ]; dependencies = { ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" || hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "i686" || hostPlatform.parsed.cpu.name == "x86_64" || (hostPlatform.parsed.cpu.name == "aarch64" || hostPlatform.parsed.cpu.name == "armv6l" || hostPlatform.parsed.cpu.name == "armv7l") && (hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "fuchsia" || hostPlatform.parsed.kernel.name == "linux") then "spin" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".spin."0.5.2" { inherit profileName; }; untrusted = rustPackages."registry+https://github.com/rust-lang/crates.io-index".untrusted."0.7.1" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "wasm32" && hostPlatform.parsed.vendor.name == "unknown" && hostPlatform.parsed.kernel.name == "unknown" && hostPlatform.parsed.abi.name == "" then "web_sys" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.56" { inherit profileName; }; @@ -5448,10 +5449,10 @@ in [ "default" ] ]; dependencies = { - ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); diff --git a/src/api/admin/cluster.rs b/src/api/admin/cluster.rs index 8e6dfb3f..010382f2 100644 --- a/src/api/admin/cluster.rs +++ b/src/api/admin/cluster.rs @@ -19,6 +19,7 @@ pub async fn handle_get_cluster_status(garage: &Arc) -> Result) -> GetClusterLayoutResponse { struct GetClusterStatusResponse { node: String, garage_version: &'static str, + garage_features: Option<&'static [&'static str]>, db_engine: String, known_nodes: HashMap, layout: GetClusterLayoutResponse, diff --git a/src/garage/admin.rs b/src/garage/admin.rs index f4c182fe..8854a58d 100644 --- a/src/garage/admin.rs +++ b/src/garage/admin.rs @@ -739,8 +739,11 @@ impl AdminRpcHandler { let mut ret = String::new(); writeln!( &mut ret, - "\nGarage version: {}", + "\nGarage version: {} [features: {}]", garage_model::version::garage_version(), + garage_model::version::garage_features() + .map(|list| list.join(", ")) + .unwrap_or_else(|| "(unknown)".into()), ) .unwrap(); writeln!(&mut ret, "\nDatabase engine: {}", self.garage.db.engine()).unwrap(); diff --git a/src/garage/main.rs b/src/garage/main.rs index 94c9bf61..7d00811a 100644 --- a/src/garage/main.rs +++ b/src/garage/main.rs @@ -25,13 +25,15 @@ use garage_rpc::system::*; use garage_rpc::*; use garage_model::helper::error::Error as HelperError; -use garage_model::version::garage_version; use admin::*; use cli::*; #[derive(StructOpt, Debug)] -#[structopt(name = "garage", version = garage_version(), about = "S3-compatible object store for self-hosted geo-distributed deployments")] +#[structopt( + name = "garage", + about = "S3-compatible object store for self-hosted geo-distributed deployments" +)] struct Opt { /// Host to connect to for admin operations, in the format: /// @: @@ -69,7 +71,35 @@ async fn main() { std::process::abort(); })); - let opt = Opt::from_args(); + // Parse opt + let features = &[ + #[cfg(feature = "k2v")] + "k2v", + #[cfg(feature = "sled")] + "sled", + #[cfg(feature = "lmdb")] + "lmdb", + #[cfg(feature = "sqlite")] + "sqlite", + #[cfg(feature = "kubernetes-discovery")] + "kubernetes-discovery", + #[cfg(feature = "metrics")] + "metrics", + #[cfg(feature = "telemetry-otlp")] + "telemetry-otlp", + #[cfg(feature = "bundled-libs")] + "bundled-libs", + #[cfg(feature = "system-libs")] + "system-libs", + ][..]; + let version = format!( + "{} [features: {}]", + garage_model::version::garage_version(), + features.join(", ") + ); + garage_model::version::init_features(features); + let opt = Opt::from_clap(&Opt::clap().version(version.as_str()).get_matches()); + let res = match opt.cmd { Command::Server => server::run_server(opt.config_file).await, Command::OfflineRepair(repair_opt) => { diff --git a/src/model/Cargo.toml b/src/model/Cargo.toml index c41d3f16..101c97d3 100644 --- a/src/model/Cargo.toml +++ b/src/model/Cargo.toml @@ -26,6 +26,7 @@ blake2 = "0.9" err-derive = "0.3" git-version = "0.3.4" hex = "0.4" +lazy_static = "1.4" base64 = "0.13" tracing = "0.1.30" rand = "0.8" diff --git a/src/model/version.rs b/src/model/version.rs index cdb3ea62..af6aa809 100644 --- a/src/model/version.rs +++ b/src/model/version.rs @@ -1,3 +1,11 @@ +use std::sync::Arc; + +use arc_swap::ArcSwapOption; + +lazy_static::lazy_static! { + static ref FEATURES: ArcSwapOption<&'static [&'static str]> = ArcSwapOption::new(None); +} + pub fn garage_version() -> &'static str { option_env!("GIT_VERSION").unwrap_or(git_version::git_version!( prefix = "git:", @@ -5,3 +13,11 @@ pub fn garage_version() -> &'static str { fallback = "unknown" )) } + +pub fn garage_features() -> Option<&'static [&'static str]> { + FEATURES.load().as_ref().map(|f| &f[..]) +} + +pub fn init_features(features: &'static [&'static str]) { + FEATURES.store(Some(Arc::new(features))); +} From 2559f63e9bb58a66da70f33e852ebbd5f909876e Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 7 Sep 2022 17:54:16 +0200 Subject: [PATCH 107/149] Make all HTTP services optionnal --- src/api/admin/api_server.rs | 19 +- src/api/k2v/api_server.rs | 14 +- src/api/s3/api_server.rs | 14 +- src/garage/server.rs | 106 ++++++---- src/util/config.rs | 6 +- src/web/lib.rs | 2 +- src/web/web_server.rs | 411 +++++++++++++++++++----------------- 7 files changed, 299 insertions(+), 273 deletions(-) diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs index d871d4e2..fb0078cc 100644 --- a/src/api/admin/api_server.rs +++ b/src/api/admin/api_server.rs @@ -1,3 +1,4 @@ +use std::net::SocketAddr; use std::sync::Arc; use async_trait::async_trait; @@ -52,15 +53,15 @@ impl AdminApiServer { } } - pub async fn run(self, shutdown_signal: impl Future) -> Result<(), GarageError> { - if let Some(bind_addr) = self.garage.config.admin.api_bind_addr { - let region = self.garage.config.s3_api.s3_region.clone(); - ApiServer::new(region, self) - .run_server(bind_addr, shutdown_signal) - .await - } else { - Ok(()) - } + pub async fn run( + self, + bind_addr: SocketAddr, + shutdown_signal: impl Future, + ) -> Result<(), GarageError> { + let region = self.garage.config.s3_api.s3_region.clone(); + ApiServer::new(region, self) + .run_server(bind_addr, shutdown_signal) + .await } fn handle_options(&self, _req: &Request) -> Result, Error> { diff --git a/src/api/k2v/api_server.rs b/src/api/k2v/api_server.rs index eb0fbdd7..084867b5 100644 --- a/src/api/k2v/api_server.rs +++ b/src/api/k2v/api_server.rs @@ -1,3 +1,4 @@ +use std::net::SocketAddr; use std::sync::Arc; use async_trait::async_trait; @@ -36,20 +37,13 @@ pub(crate) struct K2VApiEndpoint { impl K2VApiServer { pub async fn run( garage: Arc, + bind_addr: SocketAddr, + s3_region: String, shutdown_signal: impl Future, ) -> Result<(), GarageError> { - if let Some(cfg) = &garage.config.k2v_api { - let bind_addr = cfg.api_bind_addr; - - ApiServer::new( - garage.config.s3_api.s3_region.clone(), - K2VApiServer { garage }, - ) + ApiServer::new(s3_region, K2VApiServer { garage }) .run_server(bind_addr, shutdown_signal) .await - } else { - Ok(()) - } } } diff --git a/src/api/s3/api_server.rs b/src/api/s3/api_server.rs index 78dfeeac..27837297 100644 --- a/src/api/s3/api_server.rs +++ b/src/api/s3/api_server.rs @@ -1,3 +1,4 @@ +use std::net::SocketAddr; use std::sync::Arc; use async_trait::async_trait; @@ -43,16 +44,13 @@ pub(crate) struct S3ApiEndpoint { impl S3ApiServer { pub async fn run( garage: Arc, + addr: SocketAddr, + s3_region: String, shutdown_signal: impl Future, ) -> Result<(), GarageError> { - let addr = garage.config.s3_api.api_bind_addr; - - ApiServer::new( - garage.config.s3_api.s3_region.clone(), - S3ApiServer { garage }, - ) - .run_server(addr, shutdown_signal) - .await + ApiServer::new(s3_region, S3ApiServer { garage }) + .run_server(addr, shutdown_signal) + .await } async fn handle_request_without_bucket( diff --git a/src/garage/server.rs b/src/garage/server.rs index 0851738d..fb6d2279 100644 --- a/src/garage/server.rs +++ b/src/garage/server.rs @@ -9,7 +9,7 @@ use garage_util::error::Error; use garage_api::admin::api_server::AdminApiServer; use garage_api::s3::api_server::S3ApiServer; use garage_model::garage::Garage; -use garage_web::run_web_server; +use garage_web::WebServer; #[cfg(feature = "k2v")] use garage_api::k2v::api_server::K2VApiServer; @@ -30,6 +30,8 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> { info!("Loading configuration..."); let config = read_config(config_file)?; + // ---- Initialize Garage internals ---- + info!("Initializing background runner..."); let watch_cancel = netapp::util::watch_ctrl_c(); let (background, await_background_done) = BackgroundRunner::new(16, watch_cancel.clone()); @@ -44,7 +46,7 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> { init_tracing(config.admin.trace_sink.as_ref().unwrap(), garage.system.id)?; #[cfg(not(feature = "telemetry-otlp"))] - warn!("Garage was built without OTLP exporter, admin.trace_sink is ignored."); + error!("Garage was built without OTLP exporter, admin.trace_sink is ignored."); } info!("Initialize Admin API server and metrics collector..."); @@ -56,53 +58,73 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> { info!("Create admin RPC handler..."); AdminRpcHandler::new(garage.clone()); - info!("Initializing S3 API server..."); - let s3_api_server = tokio::spawn(S3ApiServer::run( - garage.clone(), - wait_from(watch_cancel.clone()), - )); + // ---- Launch public-facing API servers ---- - #[cfg(feature = "k2v")] - let k2v_api_server = { - info!("Initializing K2V API server..."); - tokio::spawn(K2VApiServer::run( - garage.clone(), - wait_from(watch_cancel.clone()), - )) - }; + let mut servers = vec![]; - info!("Initializing web server..."); - let web_server = tokio::spawn(run_web_server( - garage.clone(), - wait_from(watch_cancel.clone()), - )); + if let Some(s3_bind_addr) = &config.s3_api.api_bind_addr { + info!("Initializing S3 API server..."); + servers.push(( + "S3 API", + tokio::spawn(S3ApiServer::run( + garage.clone(), + *s3_bind_addr, + config.s3_api.s3_region.clone(), + wait_from(watch_cancel.clone()), + )), + )); + } - info!("Launching Admin API server..."); - let admin_server = tokio::spawn(admin_server.run(wait_from(watch_cancel.clone()))); + if config.k2v_api.is_some() { + #[cfg(feature = "k2v")] + { + info!("Initializing K2V API server..."); + servers.push(( + "K2V API", + tokio::spawn(K2VApiServer::run( + garage.clone(), + config.k2v_api.as_ref().unwrap().api_bind_addr, + config.s3_api.s3_region.clone(), + wait_from(watch_cancel.clone()), + )), + )); + } + #[cfg(not(feature = "k2v"))] + error!("K2V is not enabled in this build, cannot start K2V API server"); + } + + if let Some(web_config) = &config.s3_web { + info!("Initializing web server..."); + servers.push(( + "Web", + tokio::spawn(WebServer::run( + garage.clone(), + web_config.bind_addr, + web_config.root_domain.clone(), + wait_from(watch_cancel.clone()), + )), + )); + } + + if let Some(admin_bind_addr) = &config.admin.api_bind_addr { + info!("Launching Admin API server..."); + servers.push(( + "Admin", + tokio::spawn(admin_server.run(*admin_bind_addr, wait_from(watch_cancel.clone()))), + )); + } // Stuff runs // When a cancel signal is sent, stuff stops - if let Err(e) = s3_api_server.await? { - warn!("S3 API server exited with error: {}", e); - } else { - info!("S3 API server exited without error."); - } - #[cfg(feature = "k2v")] - if let Err(e) = k2v_api_server.await? { - warn!("K2V API server exited with error: {}", e); - } else { - info!("K2V API server exited without error."); - } - if let Err(e) = web_server.await? { - warn!("Web server exited with error: {}", e); - } else { - info!("Web server exited without error."); - } - if let Err(e) = admin_server.await? { - warn!("Admin web server exited with error: {}", e); - } else { - info!("Admin API server exited without error."); + + // Collect stuff + for (desc, join_handle) in servers { + if let Err(e) = join_handle.await? { + error!("{} server exited with error: {}", desc, e); + } else { + info!("{} server exited without error.", desc); + } } // Remove RPC handlers for system to break reference cycles diff --git a/src/util/config.rs b/src/util/config.rs index e8ef4fdd..46c5cb9d 100644 --- a/src/util/config.rs +++ b/src/util/config.rs @@ -81,11 +81,10 @@ pub struct Config { pub s3_api: S3ApiConfig, /// Configuration for K2V api - #[cfg(feature = "k2v")] pub k2v_api: Option, /// Configuration for serving files as normal web server - pub s3_web: WebConfig, + pub s3_web: Option, /// Configuration for the admin API endpoint #[serde(default = "Default::default")] @@ -96,7 +95,7 @@ pub struct Config { #[derive(Deserialize, Debug, Clone)] pub struct S3ApiConfig { /// Address and port to bind for api serving - pub api_bind_addr: SocketAddr, + pub api_bind_addr: Option, /// S3 region to use pub s3_region: String, /// Suffix to remove from domain name to find bucket. If None, @@ -105,7 +104,6 @@ pub struct S3ApiConfig { } /// Configuration for K2V api -#[cfg(feature = "k2v")] #[derive(Deserialize, Debug, Clone)] pub struct K2VApiConfig { /// Address and port to bind for api serving diff --git a/src/web/lib.rs b/src/web/lib.rs index 9b7c8573..7207c365 100644 --- a/src/web/lib.rs +++ b/src/web/lib.rs @@ -6,4 +6,4 @@ mod error; pub use error::Error; mod web_server; -pub use web_server::run_web_server; +pub use web_server::WebServer; diff --git a/src/web/web_server.rs b/src/web/web_server.rs index c30d8957..c2322073 100644 --- a/src/web/web_server.rs +++ b/src/web/web_server.rs @@ -57,90 +57,226 @@ impl WebMetrics { } } -/// Run a web server -pub async fn run_web_server( - garage: Arc, - shutdown_signal: impl Future, -) -> Result<(), GarageError> { - let addr = &garage.config.s3_web.bind_addr; - - let metrics = Arc::new(WebMetrics::new()); - - let service = make_service_fn(|conn: &AddrStream| { - let garage = garage.clone(); - let metrics = metrics.clone(); - - let client_addr = conn.remote_addr(); - async move { - Ok::<_, Error>(service_fn(move |req: Request| { - let garage = garage.clone(); - let metrics = metrics.clone(); - - handle_request(garage, metrics, req, client_addr) - })) - } - }); - - let server = Server::bind(addr).serve(service); - let graceful = server.with_graceful_shutdown(shutdown_signal); - info!("Web server listening on http://{}", addr); - - graceful.await?; - Ok(()) -} - -async fn handle_request( +pub struct WebServer { garage: Arc, metrics: Arc, - req: Request, - addr: SocketAddr, -) -> Result, Infallible> { - info!("{} {} {}", addr, req.method(), req.uri()); + root_domain: String, +} - // Lots of instrumentation - let tracer = opentelemetry::global::tracer("garage"); - let span = tracer - .span_builder(format!("Web {} request", req.method())) - .with_trace_id(gen_trace_id()) - .with_attributes(vec![ - KeyValue::new("method", format!("{}", req.method())), - KeyValue::new("uri", req.uri().to_string()), - ]) - .start(&tracer); +impl WebServer { + /// Run a web server + pub async fn run( + garage: Arc, + addr: SocketAddr, + root_domain: String, + shutdown_signal: impl Future, + ) -> Result<(), GarageError> { + let metrics = Arc::new(WebMetrics::new()); + let web_server = Arc::new(WebServer { + garage, + metrics, + root_domain, + }); - let metrics_tags = &[KeyValue::new("method", req.method().to_string())]; + let service = make_service_fn(|conn: &AddrStream| { + let web_server = web_server.clone(); - // The actual handler - let res = serve_file(garage, &req) - .with_context(Context::current_with_span(span)) - .record_duration(&metrics.request_duration, &metrics_tags[..]) - .await; + let client_addr = conn.remote_addr(); + async move { + Ok::<_, Error>(service_fn(move |req: Request| { + let web_server = web_server.clone(); - // More instrumentation - metrics.request_counter.add(1, &metrics_tags[..]); + web_server.handle_request(req, client_addr) + })) + } + }); - // Returning the result - match res { - Ok(res) => { - debug!("{} {} {}", req.method(), res.status(), req.uri()); - Ok(res) + let server = Server::bind(&addr).serve(service); + let graceful = server.with_graceful_shutdown(shutdown_signal); + info!("Web server listening on http://{}", addr); + + graceful.await?; + Ok(()) + } + + async fn handle_request( + self: Arc, + req: Request, + addr: SocketAddr, + ) -> Result, Infallible> { + info!("{} {} {}", addr, req.method(), req.uri()); + + // Lots of instrumentation + let tracer = opentelemetry::global::tracer("garage"); + let span = tracer + .span_builder(format!("Web {} request", req.method())) + .with_trace_id(gen_trace_id()) + .with_attributes(vec![ + KeyValue::new("method", format!("{}", req.method())), + KeyValue::new("uri", req.uri().to_string()), + ]) + .start(&tracer); + + let metrics_tags = &[KeyValue::new("method", req.method().to_string())]; + + // The actual handler + let res = self + .serve_file(&req) + .with_context(Context::current_with_span(span)) + .record_duration(&self.metrics.request_duration, &metrics_tags[..]) + .await; + + // More instrumentation + self.metrics.request_counter.add(1, &metrics_tags[..]); + + // Returning the result + match res { + Ok(res) => { + debug!("{} {} {}", req.method(), res.status(), req.uri()); + Ok(res) + } + Err(error) => { + info!( + "{} {} {} {}", + req.method(), + error.http_status_code(), + req.uri(), + error + ); + self.metrics.error_counter.add( + 1, + &[ + metrics_tags[0].clone(), + KeyValue::new("status_code", error.http_status_code().to_string()), + ], + ); + Ok(error_to_res(error)) + } } - Err(error) => { - info!( - "{} {} {} {}", - req.method(), - error.http_status_code(), - req.uri(), - error - ); - metrics.error_counter.add( - 1, - &[ - metrics_tags[0].clone(), - KeyValue::new("status_code", error.http_status_code().to_string()), - ], - ); - Ok(error_to_res(error)) + } + + async fn serve_file(self: &Arc, req: &Request) -> Result, Error> { + // Get http authority string (eg. [::1]:3902 or garage.tld:80) + let authority = req + .headers() + .get(HOST) + .ok_or_bad_request("HOST header required")? + .to_str()?; + + // Get bucket + let host = authority_to_host(authority)?; + + let bucket_name = host_to_bucket(&host, &self.root_domain).unwrap_or(&host); + let bucket_id = self + .garage + .bucket_alias_table + .get(&EmptyKey, &bucket_name.to_string()) + .await? + .and_then(|x| x.state.take()) + .ok_or(Error::NotFound)?; + + // Check bucket isn't deleted and has website access enabled + let bucket = self + .garage + .bucket_table + .get(&EmptyKey, &bucket_id) + .await? + .ok_or(Error::NotFound)?; + + let website_config = bucket + .params() + .ok_or(Error::NotFound)? + .website_config + .get() + .as_ref() + .ok_or(Error::NotFound)?; + + // Get path + let path = req.uri().path().to_string(); + let index = &website_config.index_document; + let key = path_to_key(&path, index)?; + + debug!( + "Selected bucket: \"{}\" {:?}, selected key: \"{}\"", + bucket_name, bucket_id, key + ); + + let ret_doc = match *req.method() { + Method::OPTIONS => handle_options_for_bucket(req, &bucket), + Method::HEAD => handle_head(self.garage.clone(), req, bucket_id, &key, None).await, + Method::GET => handle_get(self.garage.clone(), req, bucket_id, &key, None).await, + _ => Err(ApiError::bad_request("HTTP method not supported")), + } + .map_err(Error::from); + + match ret_doc { + Err(error) => { + // For a HEAD or OPTIONS method, and for non-4xx errors, + // we don't return the error document as content, + // we return above and just return the error message + // by relying on err_to_res that is called when we return an Err. + if *req.method() == Method::HEAD + || *req.method() == Method::OPTIONS + || !error.http_status_code().is_client_error() + { + return Err(error); + } + + // If no error document is set: just return the error directly + let error_document = match &website_config.error_document { + Some(ed) => ed.trim_start_matches('/').to_owned(), + None => return Err(error), + }; + + // We want to return the error document + // Create a fake HTTP request with path = the error document + let req2 = Request::builder() + .uri(format!("http://{}/{}", host, &error_document)) + .body(Body::empty()) + .unwrap(); + + match handle_get(self.garage.clone(), &req2, bucket_id, &error_document, None).await + { + Ok(mut error_doc) => { + // The error won't be logged back in handle_request, + // so log it here + info!( + "{} {} {} {}", + req.method(), + req.uri(), + error.http_status_code(), + error + ); + + *error_doc.status_mut() = error.http_status_code(); + error.add_headers(error_doc.headers_mut()); + + // Preserve error message in a special header + for error_line in error.to_string().split('\n') { + if let Ok(v) = HeaderValue::from_bytes(error_line.as_bytes()) { + error_doc.headers_mut().append("X-Garage-Error", v); + } + } + + Ok(error_doc) + } + Err(error_doc_error) => { + warn!( + "Couldn't get error document {} for bucket {:?}: {}", + error_document, bucket_id, error_doc_error + ); + Err(error) + } + } + } + Ok(mut resp) => { + // Maybe add CORS headers + if let Some(rule) = find_matching_cors_rule(&bucket, req)? { + add_cors_headers(&mut resp, rule) + .ok_or_internal_error("Invalid bucket CORS configuration")?; + } + Ok(resp) + } } } } @@ -160,129 +296,6 @@ fn error_to_res(e: Error) -> Response { http_error } -async fn serve_file(garage: Arc, req: &Request) -> Result, Error> { - // Get http authority string (eg. [::1]:3902 or garage.tld:80) - let authority = req - .headers() - .get(HOST) - .ok_or_bad_request("HOST header required")? - .to_str()?; - - // Get bucket - let host = authority_to_host(authority)?; - let root = &garage.config.s3_web.root_domain; - - let bucket_name = host_to_bucket(&host, root).unwrap_or(&host); - let bucket_id = garage - .bucket_alias_table - .get(&EmptyKey, &bucket_name.to_string()) - .await? - .and_then(|x| x.state.take()) - .ok_or(Error::NotFound)?; - - // Check bucket isn't deleted and has website access enabled - let bucket = garage - .bucket_table - .get(&EmptyKey, &bucket_id) - .await? - .ok_or(Error::NotFound)?; - - let website_config = bucket - .params() - .ok_or(Error::NotFound)? - .website_config - .get() - .as_ref() - .ok_or(Error::NotFound)?; - - // Get path - let path = req.uri().path().to_string(); - let index = &website_config.index_document; - let key = path_to_key(&path, index)?; - - debug!( - "Selected bucket: \"{}\" {:?}, selected key: \"{}\"", - bucket_name, bucket_id, key - ); - - let ret_doc = match *req.method() { - Method::OPTIONS => handle_options_for_bucket(req, &bucket), - Method::HEAD => handle_head(garage.clone(), req, bucket_id, &key, None).await, - Method::GET => handle_get(garage.clone(), req, bucket_id, &key, None).await, - _ => Err(ApiError::bad_request("HTTP method not supported")), - } - .map_err(Error::from); - - match ret_doc { - Err(error) => { - // For a HEAD or OPTIONS method, and for non-4xx errors, - // we don't return the error document as content, - // we return above and just return the error message - // by relying on err_to_res that is called when we return an Err. - if *req.method() == Method::HEAD - || *req.method() == Method::OPTIONS - || !error.http_status_code().is_client_error() - { - return Err(error); - } - - // If no error document is set: just return the error directly - let error_document = match &website_config.error_document { - Some(ed) => ed.trim_start_matches('/').to_owned(), - None => return Err(error), - }; - - // We want to return the error document - // Create a fake HTTP request with path = the error document - let req2 = Request::builder() - .uri(format!("http://{}/{}", host, &error_document)) - .body(Body::empty()) - .unwrap(); - - match handle_get(garage, &req2, bucket_id, &error_document, None).await { - Ok(mut error_doc) => { - // The error won't be logged back in handle_request, - // so log it here - info!( - "{} {} {} {}", - req.method(), - req.uri(), - error.http_status_code(), - error - ); - - *error_doc.status_mut() = error.http_status_code(); - error.add_headers(error_doc.headers_mut()); - - // Preserve error message in a special header - for error_line in error.to_string().split('\n') { - if let Ok(v) = HeaderValue::from_bytes(error_line.as_bytes()) { - error_doc.headers_mut().append("X-Garage-Error", v); - } - } - - Ok(error_doc) - } - Err(error_doc_error) => { - warn!( - "Couldn't get error document {} for bucket {:?}: {}", - error_document, bucket_id, error_doc_error - ); - Err(error) - } - } - } - Ok(mut resp) => { - // Maybe add CORS headers - if let Some(rule) = find_matching_cors_rule(&bucket, req)? { - add_cors_headers(&mut resp, rule) - .ok_or_internal_error("Invalid bucket CORS configuration")?; - } - Ok(resp) - } - } -} - /// Path to key /// /// Convert the provided path to the internal key From 2e00809af58c142c86fae5f4bad85c4ef5e57872 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 7 Sep 2022 17:57:12 +0200 Subject: [PATCH 108/149] Error messages when system-libs XOR bundled-libs != 1 --- src/garage/main.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/garage/main.rs b/src/garage/main.rs index 7d00811a..751dd941 100644 --- a/src/garage/main.rs +++ b/src/garage/main.rs @@ -11,6 +11,12 @@ mod server; #[cfg(feature = "telemetry-otlp")] mod tracing_setup; +#[cfg(not(any(feature = "bundled-libs", feature = "system-libs")))] +compile_error!("Either bundled-libs or system-libs Cargo feature must be enabled"); + +#[cfg(all(feature = "bundled-libs", feature = "system-libs"))] +compile_error!("Only one of bundled-libs and system-libs Cargo features must be enabled"); + use std::net::SocketAddr; use std::path::PathBuf; From 14492044394a875475b2159d51234ac1e35531bf Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 7 Sep 2022 18:02:13 +0200 Subject: [PATCH 109/149] Add warnings when features are not included in build --- src/db/lib.rs | 3 +++ src/garage/server.rs | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/src/db/lib.rs b/src/db/lib.rs index 5304c195..d96586be 100644 --- a/src/db/lib.rs +++ b/src/db/lib.rs @@ -2,6 +2,9 @@ #[cfg(feature = "sqlite")] extern crate tracing; +#[cfg(not(any(feature = "lmdb", feature = "sled", feature = "sqlite")))] +compile_error!("Must activate the Cargo feature for at least one DB engine: lmdb, sled or sqlite."); + #[cfg(feature = "lmdb")] pub mod lmdb_adapter; #[cfg(feature = "sled")] diff --git a/src/garage/server.rs b/src/garage/server.rs index fb6d2279..3c96ff22 100644 --- a/src/garage/server.rs +++ b/src/garage/server.rs @@ -114,6 +114,11 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> { )); } + #[cfg(not(feature = "metrics"))] + if config.admin_api.metrics_token.is_some() { + warn!("This Garage version is built without the metrics feature"); + } + // Stuff runs // When a cancel signal is sent, stuff stops From 107853334bd045e145e3149c63172a9c0260b8db Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 7 Sep 2022 18:10:19 +0200 Subject: [PATCH 110/149] Fix build error --- src/garage/server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/garage/server.rs b/src/garage/server.rs index 3c96ff22..aeef02a2 100644 --- a/src/garage/server.rs +++ b/src/garage/server.rs @@ -115,7 +115,7 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> { } #[cfg(not(feature = "metrics"))] - if config.admin_api.metrics_token.is_some() { + if config.admin.metrics_token.is_some() { warn!("This Garage version is built without the metrics feature"); } From 06df301de5ab2068ee55c8663eebafb0d9a26978 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 7 Sep 2022 18:16:01 +0200 Subject: [PATCH 111/149] Fix merge --- Makefile | 2 +- src/garage/cli/structs.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 1f0f3644..23e10f78 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: doc all release shell run1 run2 run3 all: - clear; cargo build --all-features + clear; cargo build release: nix-build --arg release true diff --git a/src/garage/cli/structs.rs b/src/garage/cli/structs.rs index 105e17f8..825fe859 100644 --- a/src/garage/cli/structs.rs +++ b/src/garage/cli/structs.rs @@ -502,7 +502,7 @@ pub enum WorkerCmd { opt: WorkerListOpt, }, /// Set worker parameter - #[structopt(name = "set", version = version::garage())] + #[structopt(name = "set", version = garage_version())] Set { #[structopt(subcommand)] opt: WorkerSetCmd, @@ -522,12 +522,12 @@ pub struct WorkerListOpt { #[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)] pub enum WorkerSetCmd { /// Set tranquility of scrub operations - #[structopt(name = "scrub-tranquility", version = version::garage())] + #[structopt(name = "scrub-tranquility", version = garage_version())] ScrubTranquility { tranquility: u32 }, /// Set number of concurrent block resync workers - #[structopt(name = "resync-n-workers", version = version::garage())] + #[structopt(name = "resync-n-workers", version = garage_version())] ResyncNWorkers { n_workers: usize }, /// Set tranquility of block resync operations - #[structopt(name = "resync-tranquility", version = version::garage())] + #[structopt(name = "resync-tranquility", version = garage_version())] ResyncTranquility { tranquility: u32 }, } From f310fce34b0273f9f75e7a6ea665f51003a1f795 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 7 Sep 2022 18:30:15 +0200 Subject: [PATCH 112/149] Inject GIT_VERSION even later --- nix/compile.nix | 20 ++++++++++---------- src/garage/main.rs | 9 +++++++-- src/model/version.rs | 17 +++++++++++------ 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/nix/compile.nix b/nix/compile.nix index d24cd917..512a7354 100644 --- a/nix/compile.nix +++ b/nix/compile.nix @@ -124,7 +124,15 @@ let */ (pkgs.rustBuilder.rustLib.makeOverride { name = "garage"; - overrideAttrs = drv: { + overrideAttrs = drv: + (if git_version != null then { + /* [3] */ preConfigure = '' + ${drv.preConfigure or ""} + export GIT_VERSION="${git_version}" + ''; + } else {}) + // + { /* [1] */ setBuildEnv = (buildEnv drv); /* [2] */ hardeningDisable = [ "pie" ]; }; @@ -161,15 +169,7 @@ let (pkgs.rustBuilder.rustLib.makeOverride { name = "garage_model"; - overrideAttrs = drv: - (if git_version != null then { - /* [3] */ preConfigure = '' - ${drv.preConfigure or ""} - export GIT_VERSION="${git_version}" - ''; - } else {}) - // - { /* [1] */ setBuildEnv = (buildEnv drv); }; + overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; }) (pkgs.rustBuilder.rustLib.makeOverride { diff --git a/src/garage/main.rs b/src/garage/main.rs index 751dd941..1a4a939a 100644 --- a/src/garage/main.rs +++ b/src/garage/main.rs @@ -77,7 +77,7 @@ async fn main() { std::process::abort(); })); - // Parse opt + // Initialize version and features info let features = &[ #[cfg(feature = "k2v")] "k2v", @@ -98,12 +98,17 @@ async fn main() { #[cfg(feature = "system-libs")] "system-libs", ][..]; + if let Some(git_version) = option_env!("GIT_VERSION") { + garage_model::version::init_version(git_version); + } + garage_model::version::init_features(features); + + // Parse arguments let version = format!( "{} [features: {}]", garage_model::version::garage_version(), features.join(", ") ); - garage_model::version::init_features(features); let opt = Opt::from_clap(&Opt::clap().version(version.as_str()).get_matches()); let res = match opt.cmd { diff --git a/src/model/version.rs b/src/model/version.rs index af6aa809..b515dccc 100644 --- a/src/model/version.rs +++ b/src/model/version.rs @@ -1,23 +1,28 @@ use std::sync::Arc; -use arc_swap::ArcSwapOption; +use arc_swap::{ArcSwap, ArcSwapOption}; lazy_static::lazy_static! { + static ref VERSION: ArcSwap<&'static str> = ArcSwap::new(Arc::new(git_version::git_version!( + prefix = "git:", + cargo_prefix = "cargo:", + fallback = "unknown" + ))); static ref FEATURES: ArcSwapOption<&'static [&'static str]> = ArcSwapOption::new(None); } pub fn garage_version() -> &'static str { - option_env!("GIT_VERSION").unwrap_or(git_version::git_version!( - prefix = "git:", - cargo_prefix = "cargo:", - fallback = "unknown" - )) + &VERSION.load() } pub fn garage_features() -> Option<&'static [&'static str]> { FEATURES.load().as_ref().map(|f| &f[..]) } +pub fn init_version(version: &'static str) { + VERSION.store(Arc::new(version)); +} + pub fn init_features(features: &'static [&'static str]) { FEATURES.store(Some(Arc::new(features))); } From ceb1f0229a9c8b9f8255b4a4c70272627f0c34d7 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 7 Sep 2022 18:36:46 +0200 Subject: [PATCH 113/149] Move version back into util --- Cargo.lock | 5 +++-- Cargo.nix | 11 ++++++----- src/api/admin/cluster.rs | 4 ++-- src/garage/admin.rs | 4 ++-- src/garage/cli/structs.rs | 2 +- src/garage/main.rs | 6 +++--- src/model/Cargo.toml | 2 -- src/model/lib.rs | 1 - src/util/Cargo.toml | 3 +++ src/util/lib.rs | 1 + src/{model => util}/version.rs | 0 11 files changed, 21 insertions(+), 18 deletions(-) rename src/{model => util}/version.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index eaa8f1b8..48a96462 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1099,9 +1099,7 @@ dependencies = [ "garage_rpc", "garage_table", "garage_util", - "git-version", "hex", - "lazy_static", "netapp", "opentelemetry", "rand 0.8.5", @@ -1169,15 +1167,18 @@ dependencies = [ name = "garage_util" version = "0.8.0" dependencies = [ + "arc-swap", "async-trait", "blake2", "chrono", "err-derive 0.3.1", "futures", "garage_db", + "git-version", "hex", "http", "hyper", + "lazy_static", "netapp", "opentelemetry", "rand 0.8.5", diff --git a/Cargo.nix b/Cargo.nix index 6d60a041..ad835dc7 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -1562,9 +1562,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_rpc" else null } = rustPackages."unknown".garage_rpc."0.8.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_table" else null } = rustPackages."unknown".garage_table."0.8.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "git_version" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".git-version."0.3.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "lazy_static" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; @@ -1652,15 +1650,18 @@ in (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_util") "k2v") ]; dependencies = { + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "arc_swap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "async_trait" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "blake2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".blake2."0.9.2" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "chrono" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.19" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "err_derive" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "garage_db" else null } = rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "git_version" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".git-version."0.3.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "http" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "hyper" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "lazy_static" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; @@ -2677,7 +2678,7 @@ in [ "os-poll" ] ]; dependencies = { - ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "wasi" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "wasi" || hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if hostPlatform.isWindows then "miow" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".miow."0.3.7" { inherit profileName; }; ${ if hostPlatform.isWindows then "ntapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ntapi."0.3.7" { inherit profileName; }; @@ -3833,7 +3834,7 @@ in ]; dependencies = { ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" || hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "i686" || hostPlatform.parsed.cpu.name == "x86_64" || (hostPlatform.parsed.cpu.name == "aarch64" || hostPlatform.parsed.cpu.name == "armv6l" || hostPlatform.parsed.cpu.name == "armv7l") && (hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "fuchsia" || hostPlatform.parsed.kernel.name == "linux") then "spin" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".spin."0.5.2" { inherit profileName; }; untrusted = rustPackages."registry+https://github.com/rust-lang/crates.io-index".untrusted."0.7.1" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "wasm32" && hostPlatform.parsed.vendor.name == "unknown" && hostPlatform.parsed.kernel.name == "unknown" && hostPlatform.parsed.abi.name == "" then "web_sys" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.56" { inherit profileName; }; @@ -5451,7 +5452,7 @@ in dependencies = { ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; diff --git a/src/api/admin/cluster.rs b/src/api/admin/cluster.rs index 010382f2..99c6e332 100644 --- a/src/api/admin/cluster.rs +++ b/src/api/admin/cluster.rs @@ -18,8 +18,8 @@ use crate::helpers::{json_ok_response, parse_json_body}; pub async fn handle_get_cluster_status(garage: &Arc) -> Result, Error> { let res = GetClusterStatusResponse { node: hex::encode(garage.system.id), - garage_version: garage_model::version::garage_version(), - garage_features: garage_model::version::garage_features(), + garage_version: garage_util::version::garage_version(), + garage_features: garage_util::version::garage_features(), db_engine: garage.db.engine(), known_nodes: garage .system diff --git a/src/garage/admin.rs b/src/garage/admin.rs index 9c4ecd9d..b4d2d1a1 100644 --- a/src/garage/admin.rs +++ b/src/garage/admin.rs @@ -742,8 +742,8 @@ impl AdminRpcHandler { writeln!( &mut ret, "\nGarage version: {} [features: {}]", - garage_model::version::garage_version(), - garage_model::version::garage_features() + garage_util::version::garage_version(), + garage_util::version::garage_features() .map(|list| list.join(", ")) .unwrap_or_else(|| "(unknown)".into()), ) diff --git a/src/garage/cli/structs.rs b/src/garage/cli/structs.rs index 825fe859..06548e89 100644 --- a/src/garage/cli/structs.rs +++ b/src/garage/cli/structs.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use structopt::StructOpt; -use garage_model::version::garage_version; +use garage_util::version::garage_version; #[derive(StructOpt, Debug)] pub enum Command { diff --git a/src/garage/main.rs b/src/garage/main.rs index 1a4a939a..77d5db24 100644 --- a/src/garage/main.rs +++ b/src/garage/main.rs @@ -99,14 +99,14 @@ async fn main() { "system-libs", ][..]; if let Some(git_version) = option_env!("GIT_VERSION") { - garage_model::version::init_version(git_version); + garage_util::version::init_version(git_version); } - garage_model::version::init_features(features); + garage_util::version::init_features(features); // Parse arguments let version = format!( "{} [features: {}]", - garage_model::version::garage_version(), + garage_util::version::garage_version(), features.join(", ") ); let opt = Opt::from_clap(&Opt::clap().version(version.as_str()).get_matches()); diff --git a/src/model/Cargo.toml b/src/model/Cargo.toml index 101c97d3..bbcfe89c 100644 --- a/src/model/Cargo.toml +++ b/src/model/Cargo.toml @@ -24,9 +24,7 @@ async-trait = "0.1.7" arc-swap = "1.0" blake2 = "0.9" err-derive = "0.3" -git-version = "0.3.4" hex = "0.4" -lazy_static = "1.4" base64 = "0.13" tracing = "0.1.30" rand = "0.8" diff --git a/src/model/lib.rs b/src/model/lib.rs index 43db01c5..4f20ea46 100644 --- a/src/model/lib.rs +++ b/src/model/lib.rs @@ -19,4 +19,3 @@ pub mod s3; pub mod garage; pub mod helper; pub mod migrate; -pub mod version; diff --git a/src/util/Cargo.toml b/src/util/Cargo.toml index af57008e..163c1b77 100644 --- a/src/util/Cargo.toml +++ b/src/util/Cargo.toml @@ -16,11 +16,14 @@ path = "lib.rs" [dependencies] garage_db = { version = "0.8.0", path = "../db" } +arc-swap = "1.0" async-trait = "0.1" blake2 = "0.9" err-derive = "0.3" +git-version = "0.3.4" xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] } hex = "0.4" +lazy_static = "1.4" tracing = "0.1.30" rand = "0.8" sha2 = "0.9" diff --git a/src/util/lib.rs b/src/util/lib.rs index fce151af..47c85c3a 100644 --- a/src/util/lib.rs +++ b/src/util/lib.rs @@ -14,3 +14,4 @@ pub mod persister; pub mod time; pub mod token_bucket; pub mod tranquilizer; +pub mod version; diff --git a/src/model/version.rs b/src/util/version.rs similarity index 100% rename from src/model/version.rs rename to src/util/version.rs From f91fab8582728f176f446a4a2e039d22f752167b Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 12 Sep 2022 16:23:43 +0200 Subject: [PATCH 114/149] Simplify+improve async hasher by using bounded channel --- src/util/async_hash.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/util/async_hash.rs b/src/util/async_hash.rs index fa8ee7ff..5631ea6b 100644 --- a/src/util/async_hash.rs +++ b/src/util/async_hash.rs @@ -1,7 +1,7 @@ use bytes::Bytes; use digest::Digest; -use tokio::sync::{mpsc, oneshot}; +use tokio::sync::mpsc; use tokio::task::JoinHandle; use crate::data::*; @@ -27,18 +27,17 @@ pub async fn async_blake2sum(data: Bytes) -> Hash { // ---- pub struct AsyncHasher { - sendblk: mpsc::UnboundedSender<(Bytes, oneshot::Sender<()>)>, + sendblk: mpsc::Sender, task: JoinHandle>, } impl AsyncHasher { pub fn new() -> Self { - let (sendblk, mut recvblk) = mpsc::unbounded_channel::<(Bytes, oneshot::Sender<()>)>(); + let (sendblk, mut recvblk) = mpsc::channel::(1); let task = tokio::task::spawn_blocking(move || { let mut digest = D::new(); - while let Some((blk, ch)) = recvblk.blocking_recv() { + while let Some(blk) = recvblk.blocking_recv() { digest.update(&blk[..]); - let _ = ch.send(()); } digest.finalize() }); @@ -46,9 +45,7 @@ impl AsyncHasher { } pub async fn update(&self, b: Bytes) { - let (tx, rx) = oneshot::channel(); - self.sendblk.send((b, tx)).unwrap(); - let _ = rx.await; + self.sendblk.send(b).await.unwrap(); } pub async fn finalize(self) -> digest::Output { From b823151a0bba7ee6c5f0f96c6b06355572528d94 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 12 Sep 2022 16:57:38 +0200 Subject: [PATCH 115/149] improvements in block manager --- src/block/manager.rs | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/block/manager.rs b/src/block/manager.rs index b9cd09e7..ec694fc8 100644 --- a/src/block/manager.rs +++ b/src/block/manager.rs @@ -11,7 +11,7 @@ use futures::Stream; use futures_util::stream::StreamExt; use tokio::fs; use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader}; -use tokio::sync::{mpsc, Mutex}; +use tokio::sync::{mpsc, Mutex, MutexGuard}; use opentelemetry::{ trace::{FutureExt as OtelFutureExt, TraceContextExt, Tracer}, @@ -261,7 +261,7 @@ impl BlockManager { > { let (header, stream) = self.rpc_get_raw_block_streaming(hash, order_tag).await?; match header { - DataBlockHeader::Plain => Ok(Box::pin(stream)), + DataBlockHeader::Plain => Ok(stream), DataBlockHeader::Compressed => { // Too many things, I hate it. let reader = stream_asyncread(stream); @@ -389,11 +389,7 @@ impl BlockManager { let write_size = data.inner_buffer().len() as u64; - self.mutation_lock[hash.as_slice()[0] as usize] - .lock() - .with_context(Context::current_with_span( - tracer.start("Acquire mutation_lock"), - )) + self.lock_mutate(hash) .await .write_block(hash, data, self) .bound_record_duration(&self.metrics.block_write_duration) @@ -470,8 +466,7 @@ impl BlockManager { if data.verify(*hash).is_err() { self.metrics.corruption_counter.add(1); - self.mutation_lock[hash.as_slice()[0] as usize] - .lock() + self.lock_mutate(hash) .await .move_block_to_corrupted(hash, self) .await?; @@ -484,8 +479,7 @@ impl BlockManager { /// Check if this node has a block and whether it needs it pub(crate) async fn check_block_status(&self, hash: &Hash) -> Result { - self.mutation_lock[hash.as_slice()[0] as usize] - .lock() + self.lock_mutate(hash) .await .check_block_status(hash, self) .await @@ -499,8 +493,7 @@ impl BlockManager { /// Delete block if it is not needed anymore pub(crate) async fn delete_if_unneeded(&self, hash: &Hash) -> Result<(), Error> { - self.mutation_lock[hash.as_slice()[0] as usize] - .lock() + self.lock_mutate(hash) .await .delete_if_unneeded(hash, self) .await @@ -532,6 +525,16 @@ impl BlockManager { path.set_extension(""); fs::metadata(&path).await.map(|_| false).map_err(Into::into) } + + async fn lock_mutate(&self, hash: &Hash) -> MutexGuard<'_, BlockManagerLocked> { + let tracer = opentelemetry::global::tracer("garage"); + self.mutation_lock[hash.as_slice()[0] as usize] + .lock() + .with_context(Context::current_with_span( + tracer.start("Acquire mutation_lock"), + )) + .await + } } #[async_trait] From 28a4af73ca8c0f82314157939fc98c46f338e84a Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 13 Sep 2022 13:11:44 +0200 Subject: [PATCH 116/149] Use netapp 0.5 published from crates.io --- Cargo.lock | 3 +- Cargo.nix | 73 ++++++++++++++++++++----------------------- src/garage/Cargo.toml | 3 +- src/model/Cargo.toml | 3 +- src/rpc/Cargo.toml | 3 +- src/util/Cargo.toml | 3 +- 6 files changed, 40 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a5f04e14..aaac3f1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2033,7 +2033,8 @@ dependencies = [ [[package]] name = "netapp" version = "0.5.0" -source = "git+https://git.deuxfleurs.fr/lx/netapp?branch=stream-body#0f799a7768997c37e3e1b6861c097c4cd934acde" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14fa05cce4c65d4cc5277b2525c5054fca0e51db82416d9f43fb916575233774" dependencies = [ "arc-swap", "async-trait", diff --git a/Cargo.nix b/Cargo.nix index 77df67d4..161c58cc 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -780,7 +780,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.config == "aarch64-linux-android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -1428,7 +1428,7 @@ in garage_web = rustPackages."unknown".garage_web."0.8.0" { inherit profileName; }; hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; sodiumoxide = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; - netapp = rustPackages."git+https://git.deuxfleurs.fr/lx/netapp".netapp."0.5.0" { inherit profileName; }; + netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.0" { inherit profileName; }; opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; opentelemetry_otlp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-otlp."0.10.0" { inherit profileName; }; opentelemetry_prometheus = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-prometheus."0.10.0" { inherit profileName; }; @@ -1599,7 +1599,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_table" else null } = rustPackages."unknown".garage_table."0.8.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."git+https://git.deuxfleurs.fr/lx/netapp".netapp."0.5.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; @@ -1637,7 +1637,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "k8s_openapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".k8s-openapi."0.13.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "kube" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kube."0.62.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "sodiumoxide" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."git+https://git.deuxfleurs.fr/lx/netapp".netapp."0.5.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "openssl" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".openssl."0.10.38" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "pnet_datalink" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pnet_datalink."0.28.0" { inherit profileName; }; @@ -1700,7 +1700,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "http" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "hyper" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "lazy_static" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "netapp" else null } = rustPackages."git+https://git.deuxfleurs.fr/lx/netapp".netapp."0.5.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; @@ -2742,7 +2742,7 @@ in [ "os-poll" ] ]; dependencies = { - ${ if hostPlatform.parsed.kernel.name == "wasi" || hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "wasi" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if hostPlatform.isWindows then "miow" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".miow."0.3.7" { inherit profileName; }; ${ if hostPlatform.isWindows then "ntapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ntapi."0.3.7" { inherit profileName; }; @@ -2821,42 +2821,37 @@ in }; }); - "git+https://git.deuxfleurs.fr/lx/netapp".netapp."0.5.0" = overridableMkRustCrate (profileName: rec { + "registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.0" = overridableMkRustCrate (profileName: rec { name = "netapp"; version = "0.5.0"; - registry = "git+https://git.deuxfleurs.fr/lx/netapp"; - src = fetchCrateGit { - url = https://git.deuxfleurs.fr/lx/netapp; - name = "netapp"; - version = "0.5.0"; - rev = "0f799a7768997c37e3e1b6861c097c4cd934acde"; - ref = "stream-body";}; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "14fa05cce4c65d4cc5277b2525c5054fca0e51db82416d9f43fb916575233774"; }; features = builtins.concatLists [ - [ "default" ] - [ "opentelemetry" ] - [ "opentelemetry-contrib" ] - [ "telemetry" ] + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "opentelemetry") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "opentelemetry-contrib") + (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "telemetry") ]; dependencies = { - arc_swap = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; - async_trait = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; - bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; - cfg_if = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }; - err_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; - futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; - hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; - kuska_handshake = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-handshake."0.2.0" { inherit profileName; }; - sodiumoxide = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; - log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; - opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; - opentelemetry_contrib = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-contrib."0.9.0" { inherit profileName; }; - pin_project = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; }; - rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; - rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; - serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; - tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; - tokio_stream = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.8" { inherit profileName; }; - tokio_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "arc_swap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "async_trait" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "cfg_if" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "err_derive" else null } = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "kuska_handshake" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-handshake."0.2.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "sodiumoxide" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "log" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "opentelemetry_contrib" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-contrib."0.9.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "pin_project" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio_stream" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.8" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "tokio_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.0" { inherit profileName; }; }; }); @@ -4501,7 +4496,7 @@ in ]; dependencies = { bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot_core."0.8.5" { inherit profileName; }; static_init_macro = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init_macro."1.0.2" { profileName = "__noProfile"; }; @@ -5604,7 +5599,7 @@ in ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index dcb3b78e..5ce40ff2 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -50,8 +50,7 @@ futures = "0.3" futures-util = "0.3" tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] } -#netapp = "0.4" -netapp = { version = "0.5", git = "https://git.deuxfleurs.fr/lx/netapp", branch = "stream-body", features = ["telemetry"] } +netapp = "0.5" opentelemetry = { version = "0.17", features = [ "rt-tokio" ] } opentelemetry-prometheus = { version = "0.10", optional = true } diff --git a/src/model/Cargo.toml b/src/model/Cargo.toml index d6e2adfe..2c2e2bfe 100644 --- a/src/model/Cargo.toml +++ b/src/model/Cargo.toml @@ -39,8 +39,7 @@ futures-util = "0.3" tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] } opentelemetry = "0.17" -#netapp = "0.4" -netapp = { version = "0.5", git = "https://git.deuxfleurs.fr/lx/netapp", branch = "stream-body", features = ["telemetry"] } +netapp = "0.5" [features] k2v = [ "garage_util/k2v" ] diff --git a/src/rpc/Cargo.toml b/src/rpc/Cargo.toml index d7136401..079cfe34 100644 --- a/src/rpc/Cargo.toml +++ b/src/rpc/Cargo.toml @@ -45,8 +45,7 @@ tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi tokio-stream = { version = "0.1", features = ["net"] } opentelemetry = "0.17" -#netapp = { version = "0.4.5", features = ["telemetry"] } -netapp = { version = "0.5.0", git = "https://git.deuxfleurs.fr/lx/netapp", branch = "stream-body", features = ["telemetry"] } +netapp = { version = "0.5.0", features = ["telemetry"] } hyper = { version = "0.14", features = ["client", "http1", "runtime", "tcp"] } diff --git a/src/util/Cargo.toml b/src/util/Cargo.toml index c648e13b..8e978fc2 100644 --- a/src/util/Cargo.toml +++ b/src/util/Cargo.toml @@ -39,8 +39,7 @@ toml = "0.5" futures = "0.3" tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] } -#netapp = "0.4" -netapp = { version = "0.5", git = "https://git.deuxfleurs.fr/lx/netapp", branch = "stream-body", features = ["telemetry"] } +netapp = "0.5" http = "0.2" hyper = "0.14" From ff30891999b5be5421b80b89da1037e943179d2d Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 13 Sep 2022 15:13:07 +0200 Subject: [PATCH 117/149] Use streaming block API for get with Range requests --- src/api/s3/get.rs | 93 ++++++++++++++++++++++++++++++----------------- 1 file changed, 60 insertions(+), 33 deletions(-) diff --git a/src/api/s3/get.rs b/src/api/s3/get.rs index dd95f6e7..ae4c287d 100644 --- a/src/api/s3/get.rs +++ b/src/api/s3/get.rs @@ -7,10 +7,9 @@ use http::header::{ ACCEPT_RANGES, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, ETAG, IF_MODIFIED_SINCE, IF_NONE_MATCH, LAST_MODIFIED, RANGE, }; -use hyper::body::Bytes; use hyper::{Body, Request, Response, StatusCode}; -use garage_rpc::rpc_helper::OrderTag; +use garage_rpc::rpc_helper::{netapp::stream::ByteStream, OrderTag}; use garage_table::EmptyKey; use garage_util::data::*; @@ -274,14 +273,7 @@ pub async fn handle_get( .block_manager .rpc_get_block_streaming(&hash, Some(order_stream.order(i as u64))) .await - .unwrap_or_else(|e| { - Box::pin(futures::stream::once(async move { - Err(std::io::Error::new( - std::io::ErrorKind::Other, - format!("Could not get block {}: {}", i, e), - )) - })) - }) + .unwrap_or_else(|e| error_stream(i, e)) } } }) @@ -437,44 +429,79 @@ fn body_from_blocks_range( all_blocks.len(), 4 + ((end - begin) / std::cmp::max(all_blocks[0].1.size as u64, 1024)) as usize, )); - let mut true_offset = 0; + let mut block_offset: u64 = 0; for (_, b) in all_blocks.iter() { - if true_offset >= end { + if block_offset >= end { break; } // Keep only blocks that have an intersection with the requested range - if true_offset < end && true_offset + b.size > begin { - blocks.push((*b, true_offset)); + if block_offset < end && block_offset + b.size > begin { + blocks.push((*b, block_offset)); } - true_offset += b.size; + block_offset += b.size as u64; } let order_stream = OrderTag::stream(); let body_stream = futures::stream::iter(blocks) .enumerate() - .map(move |(i, (block, true_offset))| { + .map(move |(i, (block, block_offset))| { let garage = garage.clone(); async move { - let data = garage + garage .block_manager - .rpc_get_block(&block.hash, Some(order_stream.order(i as u64))) - .await?; - let start_in_block = if true_offset > begin { - 0 - } else { - begin - true_offset - }; - let end_in_block = if true_offset + block.size < end { - block.size - } else { - end - true_offset - }; - Result::::Ok( - data.slice(start_in_block as usize..end_in_block as usize), - ) + .rpc_get_block_streaming(&block.hash, Some(order_stream.order(i as u64))) + .await + .unwrap_or_else(|e| error_stream(i, e)) + .scan(block_offset, move |chunk_offset, chunk| { + let r = match chunk { + Ok(chunk_bytes) => { + let chunk_len = chunk_bytes.len() as u64; + let r = if *chunk_offset >= end { + // The current chunk is after the part we want to read. + // Returning None here will stop the scan, the rest of the + // stream will be ignored + None + } else if *chunk_offset + chunk_len <= begin { + // The current chunk is before the part we want to read. + // We return a None that will be removed by the filter_map + // below. + Some(None) + } else { + // The chunk has an intersection with the requested range + let start_in_chunk = if *chunk_offset > begin { + 0 + } else { + begin - *chunk_offset + }; + let end_in_chunk = if *chunk_offset + chunk_len < end { + chunk_len + } else { + end - *chunk_offset + }; + Some(Some(Ok(chunk_bytes + .slice(start_in_chunk as usize..end_in_chunk as usize)))) + }; + *chunk_offset += chunk_bytes.len() as u64; + r + } + Err(e) => Some(Some(Err(e))), + }; + futures::future::ready(r) + }) + .filter_map(futures::future::ready) } }) - .buffered(2); + .buffered(2) + .flatten(); hyper::body::Body::wrap_stream(body_stream) } + +fn error_stream(i: usize, e: garage_util::error::Error) -> ByteStream { + Box::pin(futures::stream::once(async move { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Could not get block {}: {}", i, e), + )) + })) +} From 07febd3ecd0d491ed01b7ca43846aa43e423b2a1 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 13 Sep 2022 15:57:27 +0200 Subject: [PATCH 118/149] Ensure data dir is created immediately when Garage starts (fix #349) --- src/model/garage.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/model/garage.rs b/src/model/garage.rs index 66c359e7..ec1ec956 100644 --- a/src/model/garage.rs +++ b/src/model/garage.rs @@ -6,7 +6,7 @@ use garage_db as db; use garage_util::background::*; use garage_util::config::*; -use garage_util::error::Error; +use garage_util::error::*; use garage_rpc::system::System; @@ -76,9 +76,14 @@ pub struct GarageK2V { impl Garage { /// Create and run garage pub fn new(config: Config, background: Arc) -> Result, Error> { + // Create meta dir and data dir if they don't exist already + std::fs::create_dir_all(&config.metadata_dir) + .ok_or_message("Unable to create Garage metadata directory")?; + std::fs::create_dir_all(&config.data_dir) + .ok_or_message("Unable to create Garage data directory")?; + info!("Opening database..."); let mut db_path = config.metadata_dir.clone(); - std::fs::create_dir_all(&db_path).expect("Unable to create Garage meta data directory"); let db = match config.db_engine.as_str() { // ---- Sled DB ---- #[cfg(feature = "sled")] From 44733474bb6c9021c92b59e5c349b4b7ef71409a Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 13 Sep 2022 16:01:55 +0200 Subject: [PATCH 119/149] Remove/change println! in server code (fix #358) --- src/api/s3/bucket.rs | 1 - src/api/s3/copy.rs | 1 - src/rpc/kubernetes.rs | 2 +- src/table/table.rs | 1 - 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/api/s3/bucket.rs b/src/api/s3/bucket.rs index 2071fe55..3ac6a6ec 100644 --- a/src/api/s3/bucket.rs +++ b/src/api/s3/bucket.rs @@ -295,7 +295,6 @@ fn parse_create_bucket_xml(xml_bytes: &[u8]) -> Option> { let mut ret = None; for item in cbc.children() { - println!("{:?}", item); if item.has_tag_name("LocationConstraint") { if ret != None { return None; diff --git a/src/api/s3/copy.rs b/src/api/s3/copy.rs index a1a8c9a4..c5a0fc11 100644 --- a/src/api/s3/copy.rs +++ b/src/api/s3/copy.rs @@ -662,7 +662,6 @@ mod tests { last_modified: s3_xml::Value("2011-04-11T20:34:56.000Z".into()), etag: s3_xml::Value("\"9b2cf535f27731c974343645a3985328\"".into()), }; - println!("{}", to_xml_with_header(&v)?); assert_eq!(to_xml_with_header(&v)?, expected_retval); diff --git a/src/rpc/kubernetes.rs b/src/rpc/kubernetes.rs index 939a0eed..197245aa 100644 --- a/src/rpc/kubernetes.rs +++ b/src/rpc/kubernetes.rs @@ -56,7 +56,7 @@ pub async fn get_kubernetes_nodes( let mut ret = Vec::with_capacity(nodes.items.len()); for node in nodes { - println!("Found Pod: {:?}", node.metadata.name); + info!("Found Pod: {:?}", node.metadata.name); let pubkey = &node .metadata diff --git a/src/table/table.rs b/src/table/table.rs index 51f3837f..8e801be6 100644 --- a/src/table/table.rs +++ b/src/table/table.rs @@ -113,7 +113,6 @@ where async fn insert_internal(&self, e: &F::E) -> Result<(), Error> { let hash = e.partition_key().hash(); let who = self.data.replication.write_nodes(&hash); - //eprintln!("insert who: {:?}", who); let e_enc = Arc::new(ByteBuf::from(rmp_to_vec_all_named(e)?)); let rpc = TableRpc::::Update(vec![e_enc]); From 38be811b1cd20d9223b481c0ea91cc7e3ee795dc Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 13 Sep 2022 16:08:00 +0200 Subject: [PATCH 120/149] Fix clippy lint that says we should implement Eq --- src/api/s3/copy.rs | 4 +-- src/api/s3/xml.rs | 42 ++++++++++++++-------------- src/model/bucket_alias_table.rs | 2 +- src/model/bucket_table.rs | 4 +-- src/model/index_counter.rs | 2 +- src/model/key_table.rs | 4 +-- src/model/prev/v051/bucket_table.rs | 6 ++-- src/model/prev/v051/key_table.rs | 2 +- src/model/prev/v051/object_table.rs | 6 ++-- src/model/prev/v051/version_table.rs | 2 +- src/model/s3/block_ref_table.rs | 2 +- src/model/s3/object_table.rs | 6 ++-- src/model/s3/version_table.rs | 2 +- src/util/crdt/bool.rs | 2 +- src/util/crdt/deletable.rs | 2 +- src/util/crdt/lww.rs | 2 +- src/util/crdt/lww_map.rs | 2 +- src/util/crdt/map.rs | 2 +- 18 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/api/s3/copy.rs b/src/api/s3/copy.rs index c5a0fc11..7eb6459d 100644 --- a/src/api/s3/copy.rs +++ b/src/api/s3/copy.rs @@ -609,7 +609,7 @@ impl> Defragmenter { } } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct CopyObjectResult { #[serde(rename = "LastModified")] pub last_modified: s3_xml::Value, @@ -617,7 +617,7 @@ pub struct CopyObjectResult { pub etag: s3_xml::Value, } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct CopyPartResult { #[serde(serialize_with = "xmlns_tag")] pub xmlns: (), diff --git a/src/api/s3/xml.rs b/src/api/s3/xml.rs index 111657a0..06f11288 100644 --- a/src/api/s3/xml.rs +++ b/src/api/s3/xml.rs @@ -25,7 +25,7 @@ impl From<&str> for Value { #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct IntValue(#[serde(rename = "$value")] pub i64); -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct Bucket { #[serde(rename = "CreationDate")] pub creation_date: Value, @@ -33,7 +33,7 @@ pub struct Bucket { pub name: Value, } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct Owner { #[serde(rename = "DisplayName")] pub display_name: Value, @@ -41,13 +41,13 @@ pub struct Owner { pub id: Value, } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct BucketList { #[serde(rename = "Bucket")] pub entries: Vec, } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct ListAllMyBucketsResult { #[serde(rename = "Buckets")] pub buckets: BucketList, @@ -55,7 +55,7 @@ pub struct ListAllMyBucketsResult { pub owner: Owner, } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct LocationConstraint { #[serde(serialize_with = "xmlns_tag")] pub xmlns: (), @@ -63,7 +63,7 @@ pub struct LocationConstraint { pub region: String, } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct Deleted { #[serde(rename = "Key")] pub key: Value, @@ -73,7 +73,7 @@ pub struct Deleted { pub delete_marker_version_id: Value, } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct Error { #[serde(rename = "Code")] pub code: Value, @@ -85,7 +85,7 @@ pub struct Error { pub region: Option, } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct DeleteError { #[serde(rename = "Code")] pub code: Value, @@ -97,7 +97,7 @@ pub struct DeleteError { pub version_id: Option, } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct DeleteResult { #[serde(serialize_with = "xmlns_tag")] pub xmlns: (), @@ -107,7 +107,7 @@ pub struct DeleteResult { pub errors: Vec, } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct InitiateMultipartUploadResult { #[serde(serialize_with = "xmlns_tag")] pub xmlns: (), @@ -119,7 +119,7 @@ pub struct InitiateMultipartUploadResult { pub upload_id: Value, } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct CompleteMultipartUploadResult { #[serde(serialize_with = "xmlns_tag")] pub xmlns: (), @@ -133,7 +133,7 @@ pub struct CompleteMultipartUploadResult { pub etag: Value, } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct Initiator { #[serde(rename = "DisplayName")] pub display_name: Value, @@ -141,7 +141,7 @@ pub struct Initiator { pub id: Value, } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct ListMultipartItem { #[serde(rename = "Initiated")] pub initiated: Value, @@ -157,7 +157,7 @@ pub struct ListMultipartItem { pub storage_class: Value, } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct ListMultipartUploadsResult { #[serde(serialize_with = "xmlns_tag")] pub xmlns: (), @@ -187,7 +187,7 @@ pub struct ListMultipartUploadsResult { pub encoding_type: Option, } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct PartItem { #[serde(rename = "ETag")] pub etag: Value, @@ -199,7 +199,7 @@ pub struct PartItem { pub size: IntValue, } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct ListPartsResult { #[serde(serialize_with = "xmlns_tag")] pub xmlns: (), @@ -227,7 +227,7 @@ pub struct ListPartsResult { pub storage_class: Value, } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct ListBucketItem { #[serde(rename = "Key")] pub key: Value, @@ -241,13 +241,13 @@ pub struct ListBucketItem { pub storage_class: Value, } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct CommonPrefix { #[serde(rename = "Prefix")] pub prefix: Value, } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct ListBucketResult { #[serde(serialize_with = "xmlns_tag")] pub xmlns: (), @@ -281,7 +281,7 @@ pub struct ListBucketResult { pub common_prefixes: Vec, } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct VersioningConfiguration { #[serde(serialize_with = "xmlns_tag")] pub xmlns: (), @@ -289,7 +289,7 @@ pub struct VersioningConfiguration { pub status: Option, } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, PartialEq, Eq)] pub struct PostObject { #[serde(serialize_with = "xmlns_tag")] pub xmlns: (), diff --git a/src/model/bucket_alias_table.rs b/src/model/bucket_alias_table.rs index fce03d04..fcd1536e 100644 --- a/src/model/bucket_alias_table.rs +++ b/src/model/bucket_alias_table.rs @@ -7,7 +7,7 @@ use garage_table::*; /// The bucket alias table holds the names given to buckets /// in the global namespace. -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct BucketAlias { name: String, pub state: crdt::Lww>, diff --git a/src/model/bucket_table.rs b/src/model/bucket_table.rs index 130eb6a6..7be42702 100644 --- a/src/model/bucket_table.rs +++ b/src/model/bucket_table.rs @@ -12,7 +12,7 @@ use crate::permission::BucketKeyPerm; /// Its parameters are not directly accessible as: /// - It must be possible to merge paramaters, hence the use of a LWW CRDT. /// - A bucket has 2 states, Present or Deleted and parameters make sense only if present. -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct Bucket { /// ID of the bucket pub id: Uuid, @@ -21,7 +21,7 @@ pub struct Bucket { } /// Configuration for a bucket -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct BucketParams { /// Bucket's creation date pub creation_date: u64, diff --git a/src/model/index_counter.rs b/src/model/index_counter.rs index 26833390..e6394f0c 100644 --- a/src/model/index_counter.rs +++ b/src/model/index_counter.rs @@ -81,7 +81,7 @@ impl CounterEntry { } /// A counter entry in the global table -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct CounterValue { pub node_values: BTreeMap, } diff --git a/src/model/key_table.rs b/src/model/key_table.rs index 7288f6e4..9d2fc783 100644 --- a/src/model/key_table.rs +++ b/src/model/key_table.rs @@ -9,7 +9,7 @@ use crate::permission::BucketKeyPerm; use crate::prev::v051::key_table as old; /// An api key -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct Key { /// The id of the key (immutable), used as partition key pub key_id: String, @@ -19,7 +19,7 @@ pub struct Key { } /// Configuration for a key -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct KeyParams { /// The secret_key associated (immutable) pub secret_key: String, diff --git a/src/model/prev/v051/bucket_table.rs b/src/model/prev/v051/bucket_table.rs index 0c52b6ea..628a49dd 100644 --- a/src/model/prev/v051/bucket_table.rs +++ b/src/model/prev/v051/bucket_table.rs @@ -10,7 +10,7 @@ use super::key_table::PermissionSet; /// Its parameters are not directly accessible as: /// - It must be possible to merge paramaters, hence the use of a LWW CRDT. /// - A bucket has 2 states, Present or Deleted and parameters make sense only if present. -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct Bucket { /// Name of the bucket pub name: String, @@ -19,7 +19,7 @@ pub struct Bucket { } /// State of a bucket -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub enum BucketState { /// The bucket is deleted Deleted, @@ -41,7 +41,7 @@ impl Crdt for BucketState { } /// Configuration for a bucket -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct BucketParams { /// Map of key with access to the bucket, and what kind of access they give pub authorized_keys: crdt::LwwMap, diff --git a/src/model/prev/v051/key_table.rs b/src/model/prev/v051/key_table.rs index fee24741..37516b1c 100644 --- a/src/model/prev/v051/key_table.rs +++ b/src/model/prev/v051/key_table.rs @@ -4,7 +4,7 @@ use garage_table::crdt::*; use garage_table::*; /// An api key -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct Key { /// The id of the key (immutable), used as partition key pub key_id: String, diff --git a/src/model/prev/v051/object_table.rs b/src/model/prev/v051/object_table.rs index cb59b309..e79e5787 100644 --- a/src/model/prev/v051/object_table.rs +++ b/src/model/prev/v051/object_table.rs @@ -6,7 +6,7 @@ use garage_util::data::*; use garage_table::crdt::*; /// An object -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct Object { /// The bucket in which the object is stored, used as partition key pub bucket: String, @@ -26,7 +26,7 @@ impl Object { } /// Informations about a version of an object -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct ObjectVersion { /// Id of the version pub uuid: Uuid, @@ -37,7 +37,7 @@ pub struct ObjectVersion { } /// State of an object version -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub enum ObjectVersionState { /// The version is being received Uploading(ObjectVersionHeaders), diff --git a/src/model/prev/v051/version_table.rs b/src/model/prev/v051/version_table.rs index 1e658f91..c11c62d5 100644 --- a/src/model/prev/v051/version_table.rs +++ b/src/model/prev/v051/version_table.rs @@ -6,7 +6,7 @@ use garage_table::crdt::*; use garage_table::*; /// A version of an object -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct Version { /// UUID of the version, used as partition key pub uuid: Uuid, diff --git a/src/model/s3/block_ref_table.rs b/src/model/s3/block_ref_table.rs index 9589b4aa..c7017409 100644 --- a/src/model/s3/block_ref_table.rs +++ b/src/model/s3/block_ref_table.rs @@ -10,7 +10,7 @@ use garage_table::*; use garage_block::manager::*; -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct BlockRef { /// Hash (blake2 sum) of the block, used as partition key pub block: Hash, diff --git a/src/model/s3/object_table.rs b/src/model/s3/object_table.rs index a151f1b1..26ff57f6 100644 --- a/src/model/s3/object_table.rs +++ b/src/model/s3/object_table.rs @@ -21,7 +21,7 @@ pub const UNFINISHED_UPLOADS: &str = "unfinished_uploads"; pub const BYTES: &str = "bytes"; /// An object -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct Object { /// The bucket in which the object is stored, used as partition key pub bucket_id: Uuid, @@ -70,7 +70,7 @@ impl Object { } /// Informations about a version of an object -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct ObjectVersion { /// Id of the version pub uuid: Uuid, @@ -81,7 +81,7 @@ pub struct ObjectVersion { } /// State of an object version -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub enum ObjectVersionState { /// The version is being received Uploading(ObjectVersionHeaders), diff --git a/src/model/s3/version_table.rs b/src/model/s3/version_table.rs index b545e66a..6bc2ecd1 100644 --- a/src/model/s3/version_table.rs +++ b/src/model/s3/version_table.rs @@ -15,7 +15,7 @@ use crate::s3::block_ref_table::*; use crate::prev::v051::version_table as old; /// A version of an object -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct Version { /// UUID of the version, used as partition key pub uuid: Uuid, diff --git a/src/util/crdt/bool.rs b/src/util/crdt/bool.rs index 53af8f82..111eb5f1 100644 --- a/src/util/crdt/bool.rs +++ b/src/util/crdt/bool.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::crdt::crdt::*; /// Boolean, where `true` is an absorbing state -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct Bool(bool); impl Bool { diff --git a/src/util/crdt/deletable.rs b/src/util/crdt/deletable.rs index c76f5cbb..e771aceb 100644 --- a/src/util/crdt/deletable.rs +++ b/src/util/crdt/deletable.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::crdt::crdt::*; /// Deletable object (once deleted, cannot go back) -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum Deletable { Present(T), Deleted, diff --git a/src/util/crdt/lww.rs b/src/util/crdt/lww.rs index 254abe8e..958844c9 100644 --- a/src/util/crdt/lww.rs +++ b/src/util/crdt/lww.rs @@ -37,7 +37,7 @@ use crate::crdt::crdt::*; /// /// This scheme is used by AWS S3 or Soundcloud and often without knowing /// in enterprise when reconciliating databases with ad-hoc scripts. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct Lww { ts: u64, v: T, diff --git a/src/util/crdt/lww_map.rs b/src/util/crdt/lww_map.rs index 91d24c7f..88113856 100644 --- a/src/util/crdt/lww_map.rs +++ b/src/util/crdt/lww_map.rs @@ -23,7 +23,7 @@ use crate::crdt::crdt::*; /// However, note that even if we were using a more efficient data structure such as a `BTreeMap`, /// the serialization cost `O(n)` would still have to be paid at each modification, so we are /// actually not losing anything here. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct LwwMap { vals: Vec<(K, u64, V)>, } diff --git a/src/util/crdt/map.rs b/src/util/crdt/map.rs index f9ed19b6..5d1e1520 100644 --- a/src/util/crdt/map.rs +++ b/src/util/crdt/map.rs @@ -16,7 +16,7 @@ use crate::crdt::crdt::*; /// However, note that even if we were using a more efficient data structure such as a `BTreeMap`, /// the serialization cost `O(n)` would still have to be paid at each modification, so we are /// actually not losing anything here. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct Map { vals: Vec<(K, V)>, } From ab722cb40f5aacf661a280b7eb025acd3aefc1bb Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 13 Sep 2022 16:22:23 +0200 Subject: [PATCH 121/149] Add checks on replication_factor of layouts we use (fix #363, fix #364) --- src/model/garage.rs | 2 +- src/rpc/system.rs | 30 +++++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/model/garage.rs b/src/model/garage.rs index ec1ec956..75012952 100644 --- a/src/model/garage.rs +++ b/src/model/garage.rs @@ -169,7 +169,7 @@ impl Garage { background.clone(), replication_mode.replication_factor(), &config, - ); + )?; let data_rep_param = TableShardedReplication { system: system.clone(), diff --git a/src/rpc/system.rs b/src/rpc/system.rs index c0e70c61..228b66a4 100644 --- a/src/rpc/system.rs +++ b/src/rpc/system.rs @@ -198,7 +198,7 @@ impl System { background: Arc, replication_factor: usize, config: &Config, - ) -> Arc { + ) -> Result, Error> { let node_key = gen_node_key(&config.metadata_dir).expect("Unable to read or generate node ID"); info!( @@ -206,11 +206,21 @@ impl System { hex::encode(&node_key.public_key()[..8]) ); - let persist_cluster_layout = Persister::new(&config.metadata_dir, "cluster_layout"); + let persist_cluster_layout: Persister = + Persister::new(&config.metadata_dir, "cluster_layout"); let persist_peer_list = Persister::new(&config.metadata_dir, "peer_list"); let cluster_layout = match persist_cluster_layout.load() { - Ok(x) => x, + Ok(x) => { + if x.replication_factor != replication_factor { + return Err(Error::Message(format!( + "Prevous cluster layout has replication factor {}, which is different than the one specified in the config file ({}). The previous cluster layout can be purged, if you know what you are doing, simply by deleting the `cluster_layout` file in your metadata directory.", + x.replication_factor, + replication_factor + ))); + } + x + } Err(e) => { info!( "No valid previous cluster layout stored ({}), starting fresh.", @@ -303,7 +313,7 @@ impl System { metadata_dir: config.metadata_dir.clone(), }); sys.system_endpoint.set_handler(sys.clone()); - sys + Ok(sys) } /// Perform bootstraping, starting the ping loop @@ -485,7 +495,7 @@ impl System { let local_info = self.local_status.load(); if local_info.replication_factor < info.replication_factor { - error!("Some node have a higher replication factor ({}) than this one ({}). This is not supported and might lead to bugs", + error!("Some node have a higher replication factor ({}) than this one ({}). This is not supported and will lead to data corruption. Shutting down for safety.", info.replication_factor, local_info.replication_factor); std::process::exit(1); @@ -513,6 +523,16 @@ impl System { self: &Arc, adv: &ClusterLayout, ) -> Result { + if adv.replication_factor != self.replication_factor { + let msg = format!( + "Received a cluster layout from another node with replication factor {}, which is different from what we have in our configuration ({}). Discarding the cluster layout we received.", + adv.replication_factor, + self.replication_factor + ); + error!("{}", msg); + return Err(Error::Message(msg)); + } + let update_ring = self.update_ring.lock().await; let mut layout: ClusterLayout = self.ring.borrow().layout.clone(); From e46dc2a8ef8a12e49aed3883b34b538b5f65ca31 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 14 Sep 2022 16:09:38 +0200 Subject: [PATCH 122/149] Allow for hostnames in bootstrap_peers and rpc_public_addr (fix #353) --- Cargo.lock | 4 +-- Cargo.nix | 26 ++++++++--------- src/garage/main.rs | 8 ++++- src/rpc/Cargo.toml | 2 +- src/rpc/system.rs | 73 ++++++++++++++++++++++++++++++++++++---------- src/util/config.rs | 28 ++---------------- 6 files changed, 83 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aaac3f1b..9b3ecd5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2032,9 +2032,9 @@ dependencies = [ [[package]] name = "netapp" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14fa05cce4c65d4cc5277b2525c5054fca0e51db82416d9f43fb916575233774" +checksum = "06c7cbf05d7cd6e4bc51340934d60c798010bf1856769cf8508caaac1db23db6" dependencies = [ "arc-swap", "async-trait", diff --git a/Cargo.nix b/Cargo.nix index 161c58cc..f251503b 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -780,7 +780,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.config == "aarch64-linux-android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -1428,7 +1428,7 @@ in garage_web = rustPackages."unknown".garage_web."0.8.0" { inherit profileName; }; hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; sodiumoxide = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; - netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.0" { inherit profileName; }; + netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.1" { inherit profileName; }; opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; opentelemetry_otlp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-otlp."0.10.0" { inherit profileName; }; opentelemetry_prometheus = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-prometheus."0.10.0" { inherit profileName; }; @@ -1599,7 +1599,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_table" else null } = rustPackages."unknown".garage_table."0.8.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; @@ -1637,7 +1637,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "k8s_openapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".k8s-openapi."0.13.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "kube" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kube."0.62.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "sodiumoxide" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "openssl" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".openssl."0.10.38" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "pnet_datalink" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pnet_datalink."0.28.0" { inherit profileName; }; @@ -1700,7 +1700,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "http" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "hyper" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "lazy_static" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; @@ -2742,7 +2742,7 @@ in [ "os-poll" ] ]; dependencies = { - ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "wasi" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "wasi" || hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if hostPlatform.isWindows then "miow" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".miow."0.3.7" { inherit profileName; }; ${ if hostPlatform.isWindows then "ntapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ntapi."0.3.7" { inherit profileName; }; @@ -2821,11 +2821,11 @@ in }; }); - "registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.0" = overridableMkRustCrate (profileName: rec { + "registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.1" = overridableMkRustCrate (profileName: rec { name = "netapp"; - version = "0.5.0"; + version = "0.5.1"; registry = "registry+https://github.com/rust-lang/crates.io-index"; - src = fetchCratesIo { inherit name version; sha256 = "14fa05cce4c65d4cc5277b2525c5054fca0e51db82416d9f43fb916575233774"; }; + src = fetchCratesIo { inherit name version; sha256 = "06c7cbf05d7cd6e4bc51340934d60c798010bf1856769cf8508caaac1db23db6"; }; features = builtins.concatLists [ (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "opentelemetry") @@ -4496,7 +4496,7 @@ in ]; dependencies = { bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot_core."0.8.5" { inherit profileName; }; static_init_macro = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init_macro."1.0.2" { profileName = "__noProfile"; }; @@ -5597,10 +5597,10 @@ in ]; dependencies = { ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-uwp-windows-gnu" || hostPlatform.config == "i686-pc-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); diff --git a/src/garage/main.rs b/src/garage/main.rs index 0eca24ae..e5cba553 100644 --- a/src/garage/main.rs +++ b/src/garage/main.rs @@ -162,7 +162,13 @@ async fn cli_command(opt: Opt) -> Result<(), Error> { } else { let node_id = garage_rpc::system::read_node_id(&config.as_ref().unwrap().metadata_dir) .err_context(READ_KEY_ERROR)?; - if let Some(a) = config.as_ref().and_then(|c| c.rpc_public_addr) { + if let Some(a) = config.as_ref().and_then(|c| c.rpc_public_addr.as_ref()) { + use std::net::ToSocketAddrs; + let a = a + .to_socket_addrs() + .ok_or_message("unable to resolve rpc_public_addr specified in config file")? + .next() + .ok_or_message("unable to resolve rpc_public_addr specified in config file")?; (node_id, a) } else { let default_addr = SocketAddr::new( diff --git a/src/rpc/Cargo.toml b/src/rpc/Cargo.toml index 079cfe34..e51f1f73 100644 --- a/src/rpc/Cargo.toml +++ b/src/rpc/Cargo.toml @@ -45,7 +45,7 @@ tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi tokio-stream = { version = "0.1", features = ["net"] } opentelemetry = "0.17" -netapp = { version = "0.5.0", features = ["telemetry"] } +netapp = { version = "0.5.1", features = ["telemetry"] } hyper = { version = "0.14", features = ["client", "http1", "runtime", "tcp"] } diff --git a/src/rpc/system.rs b/src/rpc/system.rs index 228b66a4..2c6136a8 100644 --- a/src/rpc/system.rs +++ b/src/rpc/system.rs @@ -18,7 +18,7 @@ use tokio::sync::Mutex; use netapp::endpoint::{Endpoint, EndpointHandler}; use netapp::message::*; use netapp::peering::fullmesh::FullMeshPeeringStrategy; -use netapp::util::parse_and_resolve_peer_addr; +use netapp::util::parse_and_resolve_peer_addr_async; use netapp::{NetApp, NetworkKey, NodeID, NodeKey}; use garage_util::background::BackgroundRunner; @@ -92,7 +92,7 @@ pub struct System { rpc_listen_addr: SocketAddr, rpc_public_addr: Option, - bootstrap_peers: Vec<(NodeID, SocketAddr)>, + bootstrap_peers: Vec, consul_discovery: Option, #[cfg(feature = "kubernetes-discovery")] @@ -242,8 +242,29 @@ impl System { let ring = Ring::new(cluster_layout, replication_factor); let (update_ring, ring) = watch::channel(Arc::new(ring)); - let rpc_public_addr = match config.rpc_public_addr { - Some(a) => Some(a), + let rpc_public_addr = match &config.rpc_public_addr { + Some(a_str) => { + use std::net::ToSocketAddrs; + match a_str.to_socket_addrs() { + Err(e) => { + error!( + "Cannot resolve rpc_public_addr {} from config file: {}.", + a_str, e + ); + None + } + Ok(a) => { + let a = a.collect::>(); + if a.is_empty() { + error!("rpc_public_addr {} resolve to no known IP address", a_str); + } + if a.len() > 1 { + warn!("Multiple possible resolutions for rpc_public_addr: {:?}. Taking the first one.", a); + } + a.into_iter().next() + } + } + } None => { let addr = get_default_ip().map(|ip| SocketAddr::new(ip, config.rpc_bind_addr.port())); @@ -253,13 +274,12 @@ impl System { addr } }; + if rpc_public_addr.is_none() { + warn!("This Garage node does not know its publicly reachable RPC address, this might hamper intra-cluster communication."); + } let netapp = NetApp::new(GARAGE_VERSION_TAG, network_key, node_key); - let fullmesh = FullMeshPeeringStrategy::new( - netapp.clone(), - config.bootstrap_peers.clone(), - rpc_public_addr, - ); + let fullmesh = FullMeshPeeringStrategy::new(netapp.clone(), vec![], rpc_public_addr); let system_endpoint = netapp.endpoint(SYSTEM_RPC_PATH.into()); @@ -370,12 +390,14 @@ impl System { } pub async fn connect(&self, node: &str) -> Result<(), Error> { - let (pubkey, addrs) = parse_and_resolve_peer_addr(node).ok_or_else(|| { - Error::Message(format!( - "Unable to parse or resolve node specification: {}", - node - )) - })?; + let (pubkey, addrs) = parse_and_resolve_peer_addr_async(node) + .await + .ok_or_else(|| { + Error::Message(format!( + "Unable to parse or resolve node specification: {}", + node + )) + })?; let mut errors = vec![]; for ip in addrs.iter() { match self @@ -604,7 +626,7 @@ impl System { if not_configured || no_peers || bad_peers { info!("Doing a bootstrap/discovery step (not_configured: {}, no_peers: {}, bad_peers: {})", not_configured, no_peers, bad_peers); - let mut ping_list = self.bootstrap_peers.clone(); + let mut ping_list = resolve_peers(&self.bootstrap_peers).await; // Add peer list from list stored on disk if let Ok(peers) = self.persist_peer_list.load_async().await { @@ -735,6 +757,25 @@ fn get_default_ip() -> Option { .map(|a| a.ip()) } +async fn resolve_peers(peers: &[String]) -> Vec<(NodeID, SocketAddr)> { + let mut ret = vec![]; + + for peer in peers.iter() { + match parse_and_resolve_peer_addr_async(peer).await { + Some((pubkey, addrs)) => { + for ip in addrs { + ret.push((pubkey, ip)); + } + } + None => { + warn!("Unable to parse and/or resolve peer hostname {}", peer); + } + } + } + + ret +} + struct ConsulDiscoveryParam { consul_host: String, service_name: String, diff --git a/src/util/config.rs b/src/util/config.rs index cccad101..5e113e13 100644 --- a/src/util/config.rs +++ b/src/util/config.rs @@ -3,12 +3,8 @@ use std::io::Read; use std::net::SocketAddr; use std::path::PathBuf; -use serde::de::Error as SerdeError; use serde::{de, Deserialize}; -use netapp::util::parse_and_resolve_peer_addr; -use netapp::NodeID; - use crate::error::Error; /// Represent the whole configuration @@ -43,11 +39,11 @@ pub struct Config { /// Address to bind for RPC pub rpc_bind_addr: SocketAddr, /// Public IP address of this node - pub rpc_public_addr: Option, + pub rpc_public_addr: Option, /// Bootstrap peers RPC address - #[serde(deserialize_with = "deserialize_vec_addr", default)] - pub bootstrap_peers: Vec<(NodeID, SocketAddr)>, + #[serde(default)] + pub bootstrap_peers: Vec, /// Consul host to connect to to discover more peers pub consul_host: Option, /// Consul service name to use @@ -154,24 +150,6 @@ pub fn read_config(config_file: PathBuf) -> Result { Ok(toml::from_str(&config)?) } -fn deserialize_vec_addr<'de, D>(deserializer: D) -> Result, D::Error> -where - D: de::Deserializer<'de>, -{ - let mut ret = vec![]; - - for peer in >::deserialize(deserializer)? { - let (pubkey, addrs) = parse_and_resolve_peer_addr(peer).ok_or_else(|| { - D::Error::custom(format!("Unable to parse or resolve peer: {}", peer)) - })?; - for ip in addrs { - ret.push((pubkey, ip)); - } - } - - Ok(ret) -} - fn default_compression() -> Option { Some(1) } From 76f42a1a2b5cf088968a0730cf6de31b75f7a055 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 14 Sep 2022 17:07:55 +0200 Subject: [PATCH 123/149] Properly return HTTP 204 when deleting non-existent object (fix #227) --- src/api/s3/delete.rs | 15 +++++++-------- src/garage/tests/s3/objects.rs | 9 +++++++++ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/api/s3/delete.rs b/src/api/s3/delete.rs index 5065b285..b337155f 100644 --- a/src/api/s3/delete.rs +++ b/src/api/s3/delete.rs @@ -64,14 +64,13 @@ pub async fn handle_delete( bucket_id: Uuid, key: &str, ) -> Result, Error> { - let (_deleted_version, delete_marker_version) = - handle_delete_internal(&garage, bucket_id, key).await?; - - Ok(Response::builder() - .header("x-amz-version-id", hex::encode(delete_marker_version)) - .status(StatusCode::NO_CONTENT) - .body(Body::from(vec![])) - .unwrap()) + match handle_delete_internal(&garage, bucket_id, key).await { + Ok(_) | Err(Error::NoSuchKey) => Ok(Response::builder() + .status(StatusCode::NO_CONTENT) + .body(Body::from(vec![])) + .unwrap()), + Err(e) => Err(e), + } } pub async fn handle_delete_objects( diff --git a/src/garage/tests/s3/objects.rs b/src/garage/tests/s3/objects.rs index e1175b81..65f9e867 100644 --- a/src/garage/tests/s3/objects.rs +++ b/src/garage/tests/s3/objects.rs @@ -263,4 +263,13 @@ async fn test_deleteobject() { .unwrap(); assert!(l.contents.is_none()); + + // Deleting a non-existing object shouldn't be a problem + ctx.client + .delete_object() + .bucket(&bucket) + .key("l-0") + .send() + .await + .unwrap(); } From f6aebefcc9747bf5afad3767e9ae6f9f3aba30ae Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 14 Sep 2022 19:31:13 +0200 Subject: [PATCH 124/149] Some work on documentation towards v0.8 --- doc/book/design/benchmarks/index.md | 2 +- doc/book/design/goals.md | 4 +- doc/book/design/internals.md | 43 ++++++++++ doc/book/design/related-work.md | 2 +- doc/book/quick-start/_index.md | 9 ++ doc/book/reference-manual/admin-api.md | 2 +- doc/book/reference-manual/cli.md | 2 +- doc/book/reference-manual/configuration.md | 14 +-- doc/book/reference-manual/features.md | 85 +++++++++++++++++++ doc/book/reference-manual/k2v.md | 2 +- doc/book/reference-manual/layout.md | 2 +- doc/book/reference-manual/routing.md | 45 ---------- doc/book/reference-manual/s3-compatibility.md | 2 +- doc/book/working-documents/design-draft.md | 4 +- doc/book/working-documents/load-balancing.md | 4 +- 15 files changed, 151 insertions(+), 71 deletions(-) create mode 100644 doc/book/reference-manual/features.md delete mode 100644 doc/book/reference-manual/routing.md diff --git a/doc/book/design/benchmarks/index.md b/doc/book/design/benchmarks/index.md index c2215a4a..79cc5d62 100644 --- a/doc/book/design/benchmarks/index.md +++ b/doc/book/design/benchmarks/index.md @@ -1,6 +1,6 @@ +++ title = "Benchmarks" -weight = 10 +weight = 40 +++ With Garage, we wanted to build a software defined storage service that follow the [KISS principle](https://en.wikipedia.org/wiki/KISS_principle), diff --git a/doc/book/design/goals.md b/doc/book/design/goals.md index dea1d2c8..b97d73a9 100644 --- a/doc/book/design/goals.md +++ b/doc/book/design/goals.md @@ -1,13 +1,13 @@ +++ title = "Goals and use cases" -weight = 5 +weight = 10 +++ ## Goals and non-goals Garage is a lightweight geo-distributed data store that implements the [Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html) -object storage protocole. It enables applications to store large blobs such +object storage protocol. It enables applications to store large blobs such as pictures, video, images, documents, etc., in a redundant multi-node setting. S3 is versatile enough to also be used to publish a static website. diff --git a/doc/book/design/internals.md b/doc/book/design/internals.md index 05d852e2..777e017d 100644 --- a/doc/book/design/internals.md +++ b/doc/book/design/internals.md @@ -20,6 +20,49 @@ In the meantime, you can find some information at the following links: - [an old design draft](@/documentation/working-documents/design-draft.md) +## Request routing logic + +Data retrieval requests to Garage endpoints (S3 API and websites) are resolved +to an individual object in a bucket. Since objects are replicated to multiple nodes +Garage must ensure consistency before answering the request. + +### Using quorum to ensure consistency + +Garage ensures consistency by attempting to establish a quorum with the +data nodes responsible for the object. When a majority of the data nodes +have provided metadata on a object Garage can then answer the request. + +When a request arrives Garage will, assuming the recommended 3 replicas, perform the following actions: + +- Make a request to the two preferred nodes for object metadata +- Try the third node if one of the two initial requests fail +- Check that the metadata from at least 2 nodes match +- Check that the object hasn't been marked deleted +- Answer the request with inline data from metadata if object is small enough +- Or get data blocks from the preferred nodes and answer using the assembled object + +Garage dynamically determines which nodes to query based on health, preference, and +which nodes actually host a given data. Garage has no concept of "primary" so any +healthy node with the data can be used as long as a quorum is reached for the metadata. + +### Node health + +Garage keeps a TCP session open to each node in the cluster and periodically pings them. If a connection +cannot be established, or a node fails to answer a number of pings, the target node is marked as failed. +Failed nodes are not used for quorum or other internal requests. + +### Node preference + +Garage prioritizes which nodes to query according to a few criteria: + +- A node always prefers itself if it can answer the request +- Then the node prioritizes nodes in the same zone +- Finally the nodes with the lowest latency are prioritized + + +For further reading on the cluster structure look at the [gateway](@/documentation/cookbook/gateways.md) +and [cluster layout management](@/documentation/reference-manual/layout.md) pages. + ## Garbage collection A faulty garbage collection procedure has been the cause of diff --git a/doc/book/design/related-work.md b/doc/book/design/related-work.md index ade298ec..f96c6618 100644 --- a/doc/book/design/related-work.md +++ b/doc/book/design/related-work.md @@ -1,6 +1,6 @@ +++ title = "Related work" -weight = 15 +weight = 50 +++ ## Context diff --git a/doc/book/quick-start/_index.md b/doc/book/quick-start/_index.md index 5d7df48e..21331dcb 100644 --- a/doc/book/quick-start/_index.md +++ b/doc/book/quick-start/_index.md @@ -9,6 +9,15 @@ Let's start your Garage journey! In this chapter, we explain how to deploy Garage as a single-node server and how to interact with it. +## What is Garage? + +Before jumping in, you might be interested in reading the following pages: + +- [Goals and use cases](@/documentation/design/goals.md) +- [List of features](@/documentation/reference-manual/features.md) + +## Scope of this tutorial + Our goal is to introduce you to Garage's workflows. Following this guide is recommended before moving on to [configuring a multi-node cluster](@/documentation/cookbook/real-world.md). diff --git a/doc/book/reference-manual/admin-api.md b/doc/book/reference-manual/admin-api.md index c7316cdf..3a4a7aab 100644 --- a/doc/book/reference-manual/admin-api.md +++ b/doc/book/reference-manual/admin-api.md @@ -1,6 +1,6 @@ +++ title = "Administration API" -weight = 16 +weight = 60 +++ The Garage administration API is accessible through a dedicated server whose diff --git a/doc/book/reference-manual/cli.md b/doc/book/reference-manual/cli.md index 43a0c823..82492c3e 100644 --- a/doc/book/reference-manual/cli.md +++ b/doc/book/reference-manual/cli.md @@ -1,6 +1,6 @@ +++ title = "Garage CLI" -weight = 15 +weight = 30 +++ The Garage CLI is mostly self-documented. Make use of the `help` subcommand diff --git a/doc/book/reference-manual/configuration.md b/doc/book/reference-manual/configuration.md index 65381f46..6db12568 100644 --- a/doc/book/reference-manual/configuration.md +++ b/doc/book/reference-manual/configuration.md @@ -1,6 +1,6 @@ +++ title = "Configuration file format" -weight = 5 +weight = 20 +++ Here is an example `garage.toml` configuration file that illustrates all of the possible options: @@ -10,7 +10,6 @@ metadata_dir = "/var/lib/garage/meta" data_dir = "/var/lib/garage/data" block_size = 1048576 -block_manager_background_tranquility = 2 replication_mode = "3" @@ -87,17 +86,6 @@ files will remain available. This however means that chunks from existing files will not be deduplicated with chunks from newly uploaded files, meaning you might use more storage space that is optimally possible. -### `block_manager_background_tranquility` - -This parameter tunes the activity of the background worker responsible for -resyncing data blocks between nodes. The higher the tranquility value is set, -the more the background worker will wait between iterations, meaning the load -on the system (including network usage between nodes) will be reduced. The -minimal value for this parameter is `0`, where the background worker will -allways work at maximal throughput to resynchronize blocks. The default value -is `2`, where the background worker will try to spend at most 1/3 of its time -working, and 2/3 sleeping in order to reduce system load. - ### `replication_mode` Garage supports the following replication modes: diff --git a/doc/book/reference-manual/features.md b/doc/book/reference-manual/features.md new file mode 100644 index 00000000..23750800 --- /dev/null +++ b/doc/book/reference-manual/features.md @@ -0,0 +1,85 @@ ++++ +title = "List of Garage features" +weight = 10 ++++ + + +### S3 API + +The main goal of Garage is to provide an object storage service that is compatible with the +[S3 API](https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html) from Amazon Web Services. +We try to adhere as strictly as possible to the semantics of the API as implemented by Amazon +and other vendors such as Minio or CEPH. + +Of course Garage does not implement the full span of API endpoints that AWS S3 does; +the exact list of S3 features implemented by Garage can be found [on our S3 compatibility page](@/documentation/reference-manual/s3-compatibility.md). + +### Geo-distribution + +Garage allows you to store copies of your data in multiple geographical locations in order to maximize resilience +to adverse events, such as network/power outages or hardware failures. +This allows Garage to run very well even at home, using consumer-grade Internet connectivity +(such as FTTH) and power, as long as cluster nodes can be spawned at several physical locations. +Garage exploits knowledge of the capacity and physical location of each storage node to design +a storage plan that best exploits the available storage capacity while satisfying the geo-distributed replication constraint. + +To learn more about geo-distributed Garage clusters, +read our documentation on [setting up a real-world deployment](@/documentation/cookbook/real-world.md). + +### Flexible topology + +A Garage cluster can very easily evolve over time, as storage nodes are added or removed. +Garage will automatically rebalance data between nodes as needed to ensure the desired number of copies. +Read about cluster layout management [here](@/documentation/reference-manual/layout.md). + +### No RAFT slowing you down + +It might seem strange to tout the absence of something as a desirable feature, +but this is in fact a very important point! Garage does not use RAFT or another +consensus algorithm internally to order incoming requests: this means that all requests +directed to a Garage cluster can be handled independently of one another instead +of going through a central bottleneck (the leader node). +As a consequence, requests can be handled much faster, even in cases where latency +between cluster nodes is important (see our [benchmarks](@/documentation/design/benchmarks/index.md) for data on this). +This is particularly usefull when nodes are far from one another and talk to one other through standard Internet connections. + +### Several replication modes + +Garage supports a variety of replication modes, with 1 copy, 2 copies or 3 copies of your data, +and with various levels of consistency. +Read our reference page on [supported replication modes](@/documentation/reference-manual/configuration.md#replication-mode) +to select the replication mode best suited to your use case (hint: in most cases, `replication_mode = "3"` is what you want). + +### Web server for static websites + +A storage bucket can easily be configured to be served directly by Garage as a static web site. +Domain names for multiple websites directly map to bucket names, making it easy to build +a platform for your user's to autonomously build and host their websites over Garage. +Surprisingly, none of the other alternative S3 implementations we surveyed (such as Minio +or CEPH) support publishing static websites from S3 buckets, a feature that is however +directly inherited from S3 on AWS. + +### Bucket names as aliases + + - the same bucket may have multiple names (useful when exposing websites for example) + + - bucket renaming is possible + + - Scoped buckets: 2 users can have a different bucket with the same name -> avoid collision. Helpful if you want to write an application that creates per-user bucket always with the same name. + +### Standalone/self contained + + +### Integration with Kubernetes and Nomad + +Many node discovery methods: Kubernetes integration, Nomad integration through Consul + +### Support for changing IP addresses + +(as long as all nodes don't change their IP at the same time) + +### Cluster administration API + +### Metrics and traces + +### (experimental) K2V API diff --git a/doc/book/reference-manual/k2v.md b/doc/book/reference-manual/k2v.md index 742e4309..207d056a 100644 --- a/doc/book/reference-manual/k2v.md +++ b/doc/book/reference-manual/k2v.md @@ -1,6 +1,6 @@ +++ title = "K2V" -weight = 30 +weight = 70 +++ Starting with version 0.7.2, Garage introduces an optionnal feature, K2V, diff --git a/doc/book/reference-manual/layout.md b/doc/book/reference-manual/layout.md index 7debbf33..a7d6f51f 100644 --- a/doc/book/reference-manual/layout.md +++ b/doc/book/reference-manual/layout.md @@ -1,6 +1,6 @@ +++ title = "Cluster layout management" -weight = 10 +weight = 50 +++ The cluster layout in Garage is a table that assigns to each node a role in diff --git a/doc/book/reference-manual/routing.md b/doc/book/reference-manual/routing.md deleted file mode 100644 index aec637cc..00000000 --- a/doc/book/reference-manual/routing.md +++ /dev/null @@ -1,45 +0,0 @@ -+++ -title = "Request routing logic" -weight = 10 -+++ - -Data retrieval requests to Garage endpoints (S3 API and websites) are resolved -to an individual object in a bucket. Since objects are replicated to multiple nodes -Garage must ensure consistency before answering the request. - -## Using quorum to ensure consistency - -Garage ensures consistency by attempting to establish a quorum with the -data nodes responsible for the object. When a majority of the data nodes -have provided metadata on a object Garage can then answer the request. - -When a request arrives Garage will, assuming the recommended 3 replicas, perform the following actions: - -- Make a request to the two preferred nodes for object metadata -- Try the third node if one of the two initial requests fail -- Check that the metadata from at least 2 nodes match -- Check that the object hasn't been marked deleted -- Answer the request with inline data from metadata if object is small enough -- Or get data blocks from the preferred nodes and answer using the assembled object - -Garage dynamically determines which nodes to query based on health, preference, and -which nodes actually host a given data. Garage has no concept of "primary" so any -healthy node with the data can be used as long as a quorum is reached for the metadata. - -## Node health - -Garage keeps a TCP session open to each node in the cluster and periodically pings them. If a connection -cannot be established, or a node fails to answer a number of pings, the target node is marked as failed. -Failed nodes are not used for quorum or other internal requests. - -## Node preference - -Garage prioritizes which nodes to query according to a few criteria: - -- A node always prefers itself if it can answer the request -- Then the node prioritizes nodes in the same zone -- Finally the nodes with the lowest latency are prioritized - - -For further reading on the cluster structure look at the [gateway](@/documentation/cookbook/gateways.md) -and [cluster layout management](@/documentation/reference-manual/layout.md) pages. \ No newline at end of file diff --git a/doc/book/reference-manual/s3-compatibility.md b/doc/book/reference-manual/s3-compatibility.md index 3d571264..dd3492a0 100644 --- a/doc/book/reference-manual/s3-compatibility.md +++ b/doc/book/reference-manual/s3-compatibility.md @@ -1,6 +1,6 @@ +++ title = "S3 Compatibility status" -weight = 20 +weight = 40 +++ ## DISCLAIMER diff --git a/doc/book/working-documents/design-draft.md b/doc/book/working-documents/design-draft.md index 44849a41..3c8298b0 100644 --- a/doc/book/working-documents/design-draft.md +++ b/doc/book/working-documents/design-draft.md @@ -1,6 +1,6 @@ +++ -title = "Design draft" -weight = 25 +title = "Design draft (obsolete)" +weight = 50 +++ **WARNING: this documentation is a design draft which was written before Garage's actual implementation. diff --git a/doc/book/working-documents/load-balancing.md b/doc/book/working-documents/load-balancing.md index 87298ae6..bf6bdd95 100644 --- a/doc/book/working-documents/load-balancing.md +++ b/doc/book/working-documents/load-balancing.md @@ -1,6 +1,6 @@ +++ -title = "Load balancing data" -weight = 10 +title = "Load balancing data (obsolete)" +weight = 60 +++ **This is being yet improved in release 0.5. The working document has not been updated yet, it still only applies to Garage 0.2 through 0.4.** From 1d0a610690dbc711bf22d751ea3e6fe7047dc0a4 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Thu, 15 Sep 2022 13:23:57 +0200 Subject: [PATCH 125/149] Finish writing about Garage features, and fix from-source instructions --- doc/book/cookbook/exposing-websites.md | 4 +- doc/book/cookbook/from-source.md | 77 +++++++++++++++---------- doc/book/reference-manual/features.md | 78 +++++++++++++++++++------- 3 files changed, 110 insertions(+), 49 deletions(-) diff --git a/doc/book/cookbook/exposing-websites.md b/doc/book/cookbook/exposing-websites.md index be462dc9..5f6a5a28 100644 --- a/doc/book/cookbook/exposing-websites.md +++ b/doc/book/cookbook/exposing-websites.md @@ -5,12 +5,14 @@ weight = 25 ## Configuring a bucket for website access -There are two methods to expose buckets as website: +There are three methods to expose buckets as website: 1. using the PutBucketWebsite S3 API call, which is allowed for access keys that have the owner permission bit set 2. from the Garage CLI, by an adminstrator of the cluster +3. using the Garage administration API + The `PutBucketWebsite` API endpoint [is documented](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketWebsite.html) in the official AWS docs. This endpoint can also be called [using `aws s3api`](https://docs.aws.amazon.com/cli/latest/reference/s3api/put-bucket-website.html) on the command line. The website configuration supported by Garage is only a subset of the possibilities on Amazon S3: redirections are not supported, only the index document and error document can be specified. diff --git a/doc/book/cookbook/from-source.md b/doc/book/cookbook/from-source.md index 2b93da47..bacf93ab 100644 --- a/doc/book/cookbook/from-source.md +++ b/doc/book/cookbook/from-source.md @@ -20,57 +20,76 @@ sudo apt-get update sudo apt-get install build-essential ``` -## Using source from the Gitea repository (recommended) +## Building from source from the Gitea repository The primary location for Garage's source code is the -[Gitea repository](https://git.deuxfleurs.fr/Deuxfleurs/garage). +[Gitea repository](https://git.deuxfleurs.fr/Deuxfleurs/garage), +which contains all of the released versions as well as the code +for the developpement of the next version. -Clone the repository and build Garage with the following commands: +Clone the repository and enter it as follows: ```bash git clone https://git.deuxfleurs.fr/Deuxfleurs/garage.git cd garage -cargo build ``` -Be careful, as this will make a debug build of Garage, which will be extremely slow! -To make a release build, invoke `cargo build --release` (this takes much longer). - -The binaries built this way are found in `target/{debug,release}/garage`. - -## Using source from `crates.io` - -Garage's source code is published on `crates.io`, Rust's official package repository. -This means you can simply ask `cargo` to download and build this source code for you: +If you wish to build a specific version of Garage, check out the corresponding tag. For instance: ```bash -cargo install garage +git tag # List available tags +git checkout v0.8.0 # Change v0.8.0 with the version you wish to build ``` -That's all, `garage` should be in `$HOME/.cargo/bin`. +Otherwise you will be building a developpement build from the `main` branch +that includes all of the changes to be released in the next version. +Be careful that such a build might be unstable or contain bugs, +and could be incompatible with nodes that run stable versions of Garage. -You can add this folder to your `$PATH` or copy the binary somewhere else on your system. -For instance: +Finally, build Garage with the following command: ```bash -sudo cp $HOME/.cargo/bin/garage /usr/local/bin/garage +cargo build --release ``` +The binary built this way can now be found in `target/release/garage`. +You may simply copy this binary to somewhere in your `$PATH` in order to +have the `garage` command available in your shell, for instance: -## Selecting features to activate in your build +```bash +sudo cp target/release/garage /usr/local/bin/garage +``` -Garage supports a number of compilation options in the form of Cargo features, +If you are planning to develop Garage, +you might be interested in producing debug builds, which compile faster but run slower: +this can be done by removing the `--release` flag, and the resulting build can then +be found in `target/debug/garage`. + +## List of available Cargo feature flags + +Garage supports a number of compilation options in the form of Cargo feature flags, which can be used to provide builds adapted to your system and your use case. -The following features are available: +To produce a build with a given set of features, invoke the `cargo build` command +as follows: -| Feature | Enabled | Description | -| ------- | ------- | ----------- | -| `bundled-libs` | BY DEFAULT | Use bundled version of sqlite3, zstd, lmdb and libsodium | -| `system-libs` | optional | Use system version of sqlite3, zstd, lmdb and libsodium if available (exclusive with `bundled-libs`, build using `cargo build --no-default-features --features system-libs`) | -| `k2v` | optional | Enable the experimental K2V API (if used, all nodes on your Garage cluster must have it enabled as well) | -| `kubernetes-discovery` | optional | Enable automatic registration and discovery of cluster nodes through the Kubernetes API | -| `metrics` | BY DEFAULT | Enable collection of metrics in Prometheus format on the admin API | +```bash +# This will build the default feature set plus feature1, feature2 and feature3 +cargo build --release --features feature1,feature2,feature3 +# This will build ONLY feature1, feature2 and feature3 +cargo build --release --no-default-features \ + --features feature1,feature2,feature3 +``` + +The following feature flags are available in v0.8.0: + +| Feature flag | Enabled | Description | +| ------------ | ------- | ----------- | +| `bundled-libs` | *by default* | Use bundled version of sqlite3, zstd, lmdb and libsodium | +| `system-libs` | optional | Use system version of sqlite3, zstd, lmdb and libsodium
      if available (exclusive with `bundled-libs`, build using
      `cargo build --no-default-features --features system-libs`) | +| `k2v` | optional | Enable the experimental K2V API (if used, all nodes on your
      Garage cluster must have it enabled as well) | +| `kubernetes-discovery` | optional | Enable automatic registration and discovery
      of cluster nodes through the Kubernetes API | +| `metrics` | *by default* | Enable collection of metrics in Prometheus format on the admin API | | `telemetry-otlp` | optional | Enable collection of execution traces using OpenTelemetry | -| `sled` | BY DEFAULT | Enable using Sled to store Garage's metadata | +| `sled` | *by default* | Enable using Sled to store Garage's metadata | | `lmdb` | optional | Enable using LMDB to store Garage's metadata | | `sqlite` | optional | Enable using Sqlite3 to store Garage's metadata | diff --git a/doc/book/reference-manual/features.md b/doc/book/reference-manual/features.md index 23750800..d2d28946 100644 --- a/doc/book/reference-manual/features.md +++ b/doc/book/reference-manual/features.md @@ -26,6 +26,11 @@ a storage plan that best exploits the available storage capacity while satisfyin To learn more about geo-distributed Garage clusters, read our documentation on [setting up a real-world deployment](@/documentation/cookbook/real-world.md). +### Standalone/self-contained + +Garage is extremely simple to deploy, and does not depend on any external service to run. +This makes setting up and administering storage clusters, we hope, as easy as it could be. + ### Flexible topology A Garage cluster can very easily evolve over time, as storage nodes are added or removed. @@ -42,11 +47,11 @@ of going through a central bottleneck (the leader node). As a consequence, requests can be handled much faster, even in cases where latency between cluster nodes is important (see our [benchmarks](@/documentation/design/benchmarks/index.md) for data on this). This is particularly usefull when nodes are far from one another and talk to one other through standard Internet connections. - + ### Several replication modes Garage supports a variety of replication modes, with 1 copy, 2 copies or 3 copies of your data, -and with various levels of consistency. +and with various levels of consistency, in order to adapt to a variety of usage scenarios. Read our reference page on [supported replication modes](@/documentation/reference-manual/configuration.md#replication-mode) to select the replication mode best suited to your use case (hint: in most cases, `replication_mode = "3"` is what you want). @@ -54,32 +59,67 @@ to select the replication mode best suited to your use case (hint: in most cases A storage bucket can easily be configured to be served directly by Garage as a static web site. Domain names for multiple websites directly map to bucket names, making it easy to build -a platform for your user's to autonomously build and host their websites over Garage. +a platform for your users to autonomously build and host their websites over Garage. Surprisingly, none of the other alternative S3 implementations we surveyed (such as Minio or CEPH) support publishing static websites from S3 buckets, a feature that is however directly inherited from S3 on AWS. +Read more on our [dedicated documentation page](@/documentation/cookbook/exposing-websites.md). ### Bucket names as aliases - - the same bucket may have multiple names (useful when exposing websites for example) +In Garage, a bucket may have several names, known as aliases. +Aliases can easily be added and removed on demand: +this allows to easily rename buckets if needed +without having to copy all of their content, something that cannot be done on AWS. +For buckets served as static websites, having multiple aliases for a bucket can allow +exposing the same content under different domain names. - - bucket renaming is possible +Garage also supports bucket aliases which are local to a single user: +this allows different users to have different buckets with the same name, thus avoiding naming collisions. +This can be helpfull for instance if you want to write an application that creates per-user buckets with always the same name. - - Scoped buckets: 2 users can have a different bucket with the same name -> avoid collision. Helpful if you want to write an application that creates per-user bucket always with the same name. - -### Standalone/self contained - - -### Integration with Kubernetes and Nomad - -Many node discovery methods: Kubernetes integration, Nomad integration through Consul - -### Support for changing IP addresses - -(as long as all nodes don't change their IP at the same time) +This feature is totally invisible to S3 clients and does not break compatibility with AWS. ### Cluster administration API +Garage provides a fully-fledged REST API to administer your cluster programatically. +Functionnality included in the admin API include: setting up and monitoring +cluster nodes, managing access credentials, and managing storage buckets and bucket aliases. +A full reference of the administration API is available [here](@/documentation/reference-manual/admin-api.md). + ### Metrics and traces - -### (experimental) K2V API + +Garage makes some internal metrics available in the Prometheus data format, +which allows you to build interactive dashboards to visualize the load and internal state of your storage cluster. + +For developpers and performance-savvy administrators, +Garage also supports exporting traces of what it does internally in OpenTelemetry format. +This allows to monitor the time spent at various steps of the processing of requests, +in order to detect potential performance bottlenecks. + +### Kubernetes and Nomad integrations + +Garage can automatically discover other nodes in the cluster thanks to integration +with orchestrators such as Kubernetes and Nomad (when used with Consul). +This eases the configuration of your cluster as it removes one step where nodes need +to be manually connected to one another. + +### Support for changing IP addresses + +As long as all of your nodes don't thange their IP address at the same time, +Garage should be able to tolerate nodes with changing/dynamic IP addresses, +as nodes will regularly exchange the IP addresses of their peers and try to +reconnect using newer addresses when existing connections are broken. + +### K2V API (experimental) + +As part of an ongoing research project, Garage can expose an experimental key/value storage API called K2V. +K2V is made for the storage and retrieval of many small key/value pairs that need to be processed in bulk. +This completes the S3 API with an alternative that can be used to easily store and access metadata +related to objects stored in an S3 bucket. + +In the context of our research project, [Aérogramme](https://aerogramme.deuxfleurs.fr), +K2V is used to provide metadata and log storage for operations on encrypted e-mail storage. + +Learn more on the specification of K2V [here](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/k2v/doc/drafts/k2v-spec.md) +and on how to enable it in Garage [here](@/documentation/reference-manual/k2v.md). From 5d4b6f2173344d59d59c7f6336c5d21799f8b37d Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 19 Sep 2022 12:16:38 +0200 Subject: [PATCH 126/149] Faster GetObject workflow for getting entire objects --- Cargo.lock | 1 + Cargo.nix | 17 +++++----- src/api/Cargo.toml | 1 + src/api/s3/get.rs | 82 +++++++++++++++++++++++++++------------------- 4 files changed, 60 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9b3ecd5d..99ba4d3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1063,6 +1063,7 @@ dependencies = [ "serde_json", "sha2 0.10.2", "tokio", + "tokio-stream", "tracing", "url", ] diff --git a/Cargo.nix b/Cargo.nix index f251503b..0a472f2a 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -780,7 +780,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -1506,6 +1506,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "serde_json" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.81" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "sha2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".sha2."0.10.2" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "tokio_stream" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.8" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "tracing" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "url" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".url."2.2.2" { inherit profileName; }; }; @@ -2742,7 +2743,7 @@ in [ "os-poll" ] ]; dependencies = { - ${ if hostPlatform.parsed.kernel.name == "wasi" || hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "wasi" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }; ${ if hostPlatform.isWindows then "miow" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".miow."0.3.7" { inherit profileName; }; ${ if hostPlatform.isWindows then "ntapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ntapi."0.3.7" { inherit profileName; }; @@ -3886,7 +3887,7 @@ in ]; dependencies = { ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" || hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "i686" || hostPlatform.parsed.cpu.name == "x86_64" || (hostPlatform.parsed.cpu.name == "aarch64" || hostPlatform.parsed.cpu.name == "armv6l" || hostPlatform.parsed.cpu.name == "armv7l") && (hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "fuchsia" || hostPlatform.parsed.kernel.name == "linux") then "spin" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".spin."0.5.2" { inherit profileName; }; untrusted = rustPackages."registry+https://github.com/rust-lang/crates.io-index".untrusted."0.7.1" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "wasm32" && hostPlatform.parsed.vendor.name == "unknown" && hostPlatform.parsed.kernel.name == "unknown" && hostPlatform.parsed.abi.name == "" then "web_sys" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.56" { inherit profileName; }; @@ -4496,7 +4497,7 @@ in ]; dependencies = { bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot_core."0.8.5" { inherit profileName; }; static_init_macro = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init_macro."1.0.2" { profileName = "__noProfile"; }; @@ -5597,10 +5598,10 @@ in ]; dependencies = { ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-uwp-windows-gnu" || hostPlatform.config == "i686-pc-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml index cdfabcb8..7c3ed43b 100644 --- a/src/api/Cargo.toml +++ b/src/api/Cargo.toml @@ -38,6 +38,7 @@ futures = "0.3" futures-util = "0.3" pin-project = "1.0" tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] } +tokio-stream = "0.1" form_urlencoded = "1.0.0" http = "0.2" diff --git a/src/api/s3/get.rs b/src/api/s3/get.rs index ae4c287d..2a99551a 100644 --- a/src/api/s3/get.rs +++ b/src/api/s3/get.rs @@ -2,16 +2,19 @@ use std::sync::Arc; use std::time::{Duration, UNIX_EPOCH}; -use futures::stream::*; +use futures::future; +use futures::stream::{self, StreamExt}; use http::header::{ ACCEPT_RANGES, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, ETAG, IF_MODIFIED_SINCE, IF_NONE_MATCH, LAST_MODIFIED, RANGE, }; use hyper::{Body, Request, Response, StatusCode}; +use tokio::sync::mpsc; use garage_rpc::rpc_helper::{netapp::stream::ByteStream, OrderTag}; use garage_table::EmptyKey; use garage_util::data::*; +use garage_util::error::OkOrMessage; use garage_model::garage::Garage; use garage_model::s3::object_table::*; @@ -242,43 +245,56 @@ pub async fn handle_get( Ok(resp_builder.body(body)?) } ObjectVersionData::FirstBlock(_, first_block_hash) => { + let (tx, rx) = mpsc::channel(2); + let order_stream = OrderTag::stream(); + let first_block_hash = *first_block_hash; + let version_uuid = last_v.uuid; - let read_first_block = garage - .block_manager - .rpc_get_block_streaming(first_block_hash, Some(order_stream.order(0))); - let get_next_blocks = garage.version_table.get(&last_v.uuid, &EmptyKey); + tokio::spawn(async move { + match async { + let garage2 = garage.clone(); + let version_fut = tokio::spawn(async move { + garage2.version_table.get(&version_uuid, &EmptyKey).await + }); - let (first_block_stream, version) = - futures::try_join!(read_first_block, get_next_blocks)?; - let version = version.ok_or(Error::NoSuchKey)?; + let stream_block_0 = garage + .block_manager + .rpc_get_block_streaming(&first_block_hash, Some(order_stream.order(0))) + .await?; + tx.send(stream_block_0) + .await + .ok_or_message("channel closed")?; - let mut blocks = version - .blocks - .items() - .iter() - .map(|(_, vb)| (vb.hash, None)) - .collect::>(); - blocks[0].1 = Some(first_block_stream); - - let body_stream = futures::stream::iter(blocks) - .enumerate() - .map(move |(i, (hash, stream_opt))| { - let garage = garage.clone(); - async move { - if let Some(stream) = stream_opt { - stream - } else { - garage - .block_manager - .rpc_get_block_streaming(&hash, Some(order_stream.order(i as u64))) - .await - .unwrap_or_else(|e| error_stream(i, e)) - } + let version = version_fut.await.unwrap()?.ok_or(Error::NoSuchKey)?; + for (i, (_, vb)) in version.blocks.items().iter().enumerate().skip(1) { + let stream_block_i = garage + .block_manager + .rpc_get_block_streaming(&vb.hash, Some(order_stream.order(i as u64))) + .await?; + tx.send(stream_block_i) + .await + .ok_or_message("channel closed")?; } - }) - .buffered(2) - .flatten(); + + Ok::<(), Error>(()) + } + .await + { + Ok(()) => (), + Err(e) => { + let err = std::io::Error::new( + std::io::ErrorKind::Other, + format!("Error while getting object data: {}", e), + ); + let _ = tx + .send(Box::pin(stream::once(future::ready(Err(err))))) + .await; + } + } + }); + + let body_stream = tokio_stream::wrappers::ReceiverStream::new(rx).flatten(); let body = hyper::body::Body::wrap_stream(body_stream); Ok(resp_builder.body(body)?) From 56592e18538b379ccaaa7b7c1990a599ac83b191 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 19 Sep 2022 20:12:19 +0200 Subject: [PATCH 127/149] RPC performance changes - configurable ping timeout - single, much higher, configurable RPC timeout - no more concurrency semaphore --- Cargo.lock | 4 +-- Cargo.nix | 26 ++++++++++---------- src/block/manager.rs | 18 +++++++------- src/block/resync.rs | 14 ++--------- src/model/k2v/rpc.rs | 36 +++++++++++++-------------- src/rpc/Cargo.toml | 2 +- src/rpc/metrics.rs | 19 +-------------- src/rpc/rpc_helper.rs | 57 +++++++++++++++++++++---------------------- src/rpc/system.rs | 16 +++++++++--- src/table/gc.rs | 10 ++------ src/table/sync.rs | 16 +++--------- src/table/table.rs | 14 +++-------- src/util/config.rs | 5 ++++ 13 files changed, 99 insertions(+), 138 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 99ba4d3a..94a44918 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2033,9 +2033,9 @@ dependencies = [ [[package]] name = "netapp" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06c7cbf05d7cd6e4bc51340934d60c798010bf1856769cf8508caaac1db23db6" +checksum = "4ffe47ac46d3b2ce2f736a70865492df082e042eb2bfdddfca3b8dd66bd9469d" dependencies = [ "arc-swap", "async-trait", diff --git a/Cargo.nix b/Cargo.nix index 0a472f2a..34e3363a 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -780,7 +780,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.config == "aarch64-linux-android" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -1428,7 +1428,7 @@ in garage_web = rustPackages."unknown".garage_web."0.8.0" { inherit profileName; }; hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; sodiumoxide = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; - netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.1" { inherit profileName; }; + netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.2" { inherit profileName; }; opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; opentelemetry_otlp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-otlp."0.10.0" { inherit profileName; }; opentelemetry_prometheus = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-prometheus."0.10.0" { inherit profileName; }; @@ -1600,7 +1600,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_table" else null } = rustPackages."unknown".garage_table."0.8.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.2" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_web" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; @@ -1638,7 +1638,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "k8s_openapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".k8s-openapi."0.13.1" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "kube" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kube."0.62.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "sodiumoxide" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.2" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "openssl" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".openssl."0.10.38" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "pnet_datalink" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pnet_datalink."0.28.0" { inherit profileName; }; @@ -1701,7 +1701,7 @@ in ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "http" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "hyper" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "lazy_static" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { inherit profileName; }; - ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.1" { inherit profileName; }; + ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "netapp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.2" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; ${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "rmp_serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }; @@ -2822,11 +2822,11 @@ in }; }); - "registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.1" = overridableMkRustCrate (profileName: rec { + "registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.2" = overridableMkRustCrate (profileName: rec { name = "netapp"; - version = "0.5.1"; + version = "0.5.2"; registry = "registry+https://github.com/rust-lang/crates.io-index"; - src = fetchCratesIo { inherit name version; sha256 = "06c7cbf05d7cd6e4bc51340934d60c798010bf1856769cf8508caaac1db23db6"; }; + src = fetchCratesIo { inherit name version; sha256 = "4ffe47ac46d3b2ce2f736a70865492df082e042eb2bfdddfca3b8dd66bd9469d"; }; features = builtins.concatLists [ (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_util" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default") (lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "opentelemetry") @@ -3887,7 +3887,7 @@ in ]; dependencies = { ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" || hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "i686" || hostPlatform.parsed.cpu.name == "x86_64" || (hostPlatform.parsed.cpu.name == "aarch64" || hostPlatform.parsed.cpu.name == "armv6l" || hostPlatform.parsed.cpu.name == "armv7l") && (hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "fuchsia" || hostPlatform.parsed.kernel.name == "linux") then "spin" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".spin."0.5.2" { inherit profileName; }; untrusted = rustPackages."registry+https://github.com/rust-lang/crates.io-index".untrusted."0.7.1" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "wasm32" && hostPlatform.parsed.vendor.name == "unknown" && hostPlatform.parsed.kernel.name == "unknown" && hostPlatform.parsed.abi.name == "" then "web_sys" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.56" { inherit profileName; }; @@ -4497,7 +4497,7 @@ in ]; dependencies = { bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot_core."0.8.5" { inherit profileName; }; static_init_macro = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init_macro."1.0.2" { profileName = "__noProfile"; }; @@ -5597,11 +5597,11 @@ in [ "default" ] ]; dependencies = { - ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); diff --git a/src/block/manager.rs b/src/block/manager.rs index ec694fc8..7f439b96 100644 --- a/src/block/manager.rs +++ b/src/block/manager.rs @@ -41,9 +41,6 @@ use crate::resync::*; /// Size under which data will be stored inlined in database instead of as files pub const INLINE_THRESHOLD: usize = 3072; -// Timeout for RPCs that read and write blocks to remote nodes -pub(crate) const BLOCK_RW_TIMEOUT: Duration = Duration::from_secs(60); - // The delay between the moment when the reference counter // drops to zero, and the moment where we allow ourselves // to delete the block locally. @@ -183,7 +180,7 @@ impl BlockManager { }; return Ok((header, stream)); } - _ = tokio::time::sleep(BLOCK_RW_TIMEOUT) => { + _ = tokio::time::sleep(self.system.rpc.rpc_timeout()) => { debug!("Node {:?} didn't return block in time, trying next.", node); } }; @@ -235,7 +232,7 @@ impl BlockManager { } } } - _ = tokio::time::sleep(BLOCK_RW_TIMEOUT) => { + _ = tokio::time::sleep(self.system.rpc.rpc_timeout()) => { debug!("Node {:?} didn't return block in time, trying next.", node); } }; @@ -300,8 +297,7 @@ impl BlockManager { &who[..], put_block_rpc, RequestStrategy::with_priority(PRIO_NORMAL | PRIO_SECONDARY) - .with_quorum(self.replication.write_quorum()) - .with_timeout(BLOCK_RW_TIMEOUT), + .with_quorum(self.replication.write_quorum()), ) .await?; @@ -336,7 +332,10 @@ impl BlockManager { // we will fecth it from someone. let this = self.clone(); tokio::spawn(async move { - if let Err(e) = this.resync.put_to_resync(&hash, 2 * BLOCK_RW_TIMEOUT) { + if let Err(e) = this + .resync + .put_to_resync(&hash, 2 * this.system.rpc.rpc_timeout()) + { error!("Block {:?} could not be put in resync queue: {}.", hash, e); } }); @@ -444,7 +443,8 @@ impl BlockManager { Ok(c) => c, Err(e) => { // Not found but maybe we should have had it ?? - self.resync.put_to_resync(hash, 2 * BLOCK_RW_TIMEOUT)?; + self.resync + .put_to_resync(hash, 2 * self.system.rpc.rpc_timeout())?; return Err(Into::into(e)); } }; diff --git a/src/block/resync.rs b/src/block/resync.rs index bde3e98c..ada3ac54 100644 --- a/src/block/resync.rs +++ b/src/block/resync.rs @@ -33,14 +33,6 @@ use garage_table::replication::TableReplication; use crate::manager::*; -// Timeout for RPCs that ask other nodes whether they need a copy -// of a given block before we delete it locally -// The timeout here is relatively low because we don't want to block -// the entire resync loop when some nodes are not responding. -// Nothing will be deleted if the nodes don't answer the queries, -// we will just retry later. -const NEED_BLOCK_QUERY_TIMEOUT: Duration = Duration::from_secs(15); - // The delay between the time where a resync operation fails // and the time when it is retried, with exponential backoff // (multiplied by 2, 4, 8, 16, etc. for every consecutive failure). @@ -346,8 +338,7 @@ impl BlockResyncManager { &manager.endpoint, &who, BlockRpc::NeedBlockQuery(*hash), - RequestStrategy::with_priority(PRIO_BACKGROUND) - .with_timeout(NEED_BLOCK_QUERY_TIMEOUT), + RequestStrategy::with_priority(PRIO_BACKGROUND), ) .await?; @@ -394,8 +385,7 @@ impl BlockResyncManager { &need_nodes[..], put_block_message, RequestStrategy::with_priority(PRIO_BACKGROUND) - .with_quorum(need_nodes.len()) - .with_timeout(BLOCK_RW_TIMEOUT), + .with_quorum(need_nodes.len()), ) .await .err_context("PutBlock RPC")?; diff --git a/src/model/k2v/rpc.rs b/src/model/k2v/rpc.rs index 90101d0f..a74df277 100644 --- a/src/model/k2v/rpc.rs +++ b/src/model/k2v/rpc.rs @@ -23,7 +23,6 @@ use garage_rpc::system::System; use garage_rpc::*; use garage_table::replication::{TableReplication, TableShardedReplication}; -use garage_table::table::TABLE_RPC_TIMEOUT; use garage_table::{PartitionKey, Table}; use crate::k2v::causality::*; @@ -117,7 +116,6 @@ impl K2VRpcHandler { }), RequestStrategy::with_priority(PRIO_NORMAL) .with_quorum(1) - .with_timeout(TABLE_RPC_TIMEOUT) .interrupt_after_quorum(true), ) .await?; @@ -169,7 +167,6 @@ impl K2VRpcHandler { K2VRpc::InsertManyItems(items), RequestStrategy::with_priority(PRIO_NORMAL) .with_quorum(1) - .with_timeout(TABLE_RPC_TIMEOUT) .interrupt_after_quorum(true), ) .await?; @@ -205,22 +202,23 @@ impl K2VRpcHandler { .replication .write_nodes(&poll_key.partition.hash()); - let resps = self - .system - .rpc - .try_call_many( - &self.endpoint, - &nodes[..], - K2VRpc::PollItem { - key: poll_key, - causal_context, - timeout_msec, - }, - RequestStrategy::with_priority(PRIO_NORMAL) - .with_quorum(self.item_table.data.replication.read_quorum()) - .with_timeout(Duration::from_millis(timeout_msec) + TABLE_RPC_TIMEOUT), - ) - .await?; + let rpc = self.system.rpc.try_call_many( + &self.endpoint, + &nodes[..], + K2VRpc::PollItem { + key: poll_key, + causal_context, + timeout_msec, + }, + RequestStrategy::with_priority(PRIO_NORMAL) + .with_quorum(self.item_table.data.replication.read_quorum()) + .without_timeout(), + ); + let timeout_duration = Duration::from_millis(timeout_msec) + self.system.rpc.rpc_timeout(); + let resps = select! { + r = rpc => r?, + _ = tokio::time::sleep(timeout_duration) => return Ok(None), + }; let mut resp: Option = None; for v in resps { diff --git a/src/rpc/Cargo.toml b/src/rpc/Cargo.toml index e51f1f73..d61acea4 100644 --- a/src/rpc/Cargo.toml +++ b/src/rpc/Cargo.toml @@ -45,7 +45,7 @@ tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi tokio-stream = { version = "0.1", features = ["net"] } opentelemetry = "0.17" -netapp = { version = "0.5.1", features = ["telemetry"] } +netapp = { version = "0.5.2", features = ["telemetry"] } hyper = { version = "0.14", features = ["client", "http1", "runtime", "tcp"] } diff --git a/src/rpc/metrics.rs b/src/rpc/metrics.rs index c900518c..61f8fa79 100644 --- a/src/rpc/metrics.rs +++ b/src/rpc/metrics.rs @@ -1,31 +1,18 @@ -use std::sync::Arc; - use opentelemetry::{global, metrics::*}; -use tokio::sync::Semaphore; /// TableMetrics reference all counter used for metrics pub struct RpcMetrics { - pub(crate) _rpc_available_permits: ValueObserver, - pub(crate) rpc_counter: Counter, pub(crate) rpc_timeout_counter: Counter, pub(crate) rpc_netapp_error_counter: Counter, pub(crate) rpc_garage_error_counter: Counter, pub(crate) rpc_duration: ValueRecorder, - pub(crate) rpc_queueing_time: ValueRecorder, } impl RpcMetrics { - pub fn new(sem: Arc) -> Self { + pub fn new() -> Self { let meter = global::meter("garage_rpc"); RpcMetrics { - _rpc_available_permits: meter - .u64_value_observer("rpc.available_permits", move |observer| { - observer.observe(sem.available_permits() as u64, &[]) - }) - .with_description("Number of available RPC permits") - .init(), - rpc_counter: meter .u64_counter("rpc.request_counter") .with_description("Number of RPC requests emitted") @@ -46,10 +33,6 @@ impl RpcMetrics { .f64_value_recorder("rpc.duration") .with_description("Duration of RPCs") .init(), - rpc_queueing_time: meter - .f64_value_recorder("rpc.queueing_time") - .with_description("Time RPC requests were queued for before being sent") - .init(), } } } diff --git a/src/rpc/rpc_helper.rs b/src/rpc/rpc_helper.rs index 19abb4c5..857ed620 100644 --- a/src/rpc/rpc_helper.rs +++ b/src/rpc/rpc_helper.rs @@ -7,7 +7,7 @@ use futures::stream::futures_unordered::FuturesUnordered; use futures::stream::StreamExt; use futures_util::future::FutureExt; use tokio::select; -use tokio::sync::{watch, Semaphore}; +use tokio::sync::watch; use opentelemetry::KeyValue; use opentelemetry::{ @@ -32,32 +32,30 @@ use garage_util::metrics::RecordDuration; use crate::metrics::RpcMetrics; use crate::ring::Ring; -const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30); - -// Don't allow more than 100 concurrent outgoing RPCs. -const MAX_CONCURRENT_REQUESTS: usize = 100; +// Default RPC timeout = 5 minutes +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(300); /// Strategy to apply when making RPC #[derive(Copy, Clone)] pub struct RequestStrategy { - /// Max time to wait for reponse - pub rs_timeout: Duration, /// Min number of response to consider the request successful pub rs_quorum: Option, /// Should requests be dropped after enough response are received pub rs_interrupt_after_quorum: bool, /// Request priority pub rs_priority: RequestPriority, + /// Deactivate timeout for this request + pub rs_no_timeout: bool, } impl RequestStrategy { /// Create a RequestStrategy with default timeout and not interrupting when quorum reached pub fn with_priority(prio: RequestPriority) -> Self { RequestStrategy { - rs_timeout: DEFAULT_TIMEOUT, rs_quorum: None, rs_interrupt_after_quorum: false, rs_priority: prio, + rs_no_timeout: false, } } /// Set quorum to be reached for request @@ -65,17 +63,17 @@ impl RequestStrategy { self.rs_quorum = Some(quorum); self } - /// Set timeout of the strategy - pub fn with_timeout(mut self, timeout: Duration) -> Self { - self.rs_timeout = timeout; - self - } /// Set if requests can be dropped after quorum has been reached /// In general true for read requests, and false for write pub fn interrupt_after_quorum(mut self, interrupt: bool) -> Self { self.rs_interrupt_after_quorum = interrupt; self } + /// Deactivate timeout for this request + pub fn without_timeout(mut self) -> Self { + self.rs_no_timeout = true; + self + } } #[derive(Clone)] @@ -86,8 +84,8 @@ struct RpcHelperInner { fullmesh: Arc, background: Arc, ring: watch::Receiver>, - request_buffer_semaphore: Arc, metrics: RpcMetrics, + rpc_timeout: Duration, } impl RpcHelper { @@ -96,21 +94,24 @@ impl RpcHelper { fullmesh: Arc, background: Arc, ring: watch::Receiver>, + rpc_timeout: Option, ) -> Self { - let sem = Arc::new(Semaphore::new(MAX_CONCURRENT_REQUESTS)); - - let metrics = RpcMetrics::new(sem.clone()); + let metrics = RpcMetrics::new(); Self(Arc::new(RpcHelperInner { our_node_id, fullmesh, background, ring, - request_buffer_semaphore: sem, metrics, + rpc_timeout: rpc_timeout.unwrap_or(DEFAULT_TIMEOUT), })) } + pub fn rpc_timeout(&self) -> Duration { + self.0.rpc_timeout + } + pub async fn call( &self, endpoint: &Endpoint, @@ -129,13 +130,6 @@ impl RpcHelper { KeyValue::new("to", format!("{:?}", to)), ]; - let permit = self - .0 - .request_buffer_semaphore - .acquire() - .record_duration(&self.0.metrics.rpc_queueing_time, &metric_tags) - .await?; - self.0.metrics.rpc_counter.add(1, &metric_tags); let node_id = to.into(); @@ -143,10 +137,16 @@ impl RpcHelper { .call_streaming(&node_id, msg, strat.rs_priority) .record_duration(&self.0.metrics.rpc_duration, &metric_tags); + let timeout = async { + if strat.rs_no_timeout { + futures::future::pending().await + } else { + tokio::time::sleep(self.0.rpc_timeout).await + } + }; + select! { res = rpc_call => { - drop(permit); - if res.is_err() { self.0.metrics.rpc_netapp_error_counter.add(1, &metric_tags); } @@ -158,8 +158,7 @@ impl RpcHelper { Ok(res?) } - _ = tokio::time::sleep(strat.rs_timeout) => { - drop(permit); + () = timeout => { self.0.metrics.rpc_timeout_counter.add(1, &metric_tags); Err(Error::Timeout) } diff --git a/src/rpc/system.rs b/src/rpc/system.rs index 2c6136a8..f8121193 100644 --- a/src/rpc/system.rs +++ b/src/rpc/system.rs @@ -37,7 +37,6 @@ use crate::rpc_helper::*; const DISCOVERY_INTERVAL: Duration = Duration::from_secs(60); const STATUS_EXCHANGE_INTERVAL: Duration = Duration::from_secs(10); -const SYSTEM_RPC_TIMEOUT: Duration = Duration::from_secs(15); /// Version tag used for version check upon Netapp connection. /// Cluster nodes with different version tags are deemed @@ -280,6 +279,9 @@ impl System { let netapp = NetApp::new(GARAGE_VERSION_TAG, network_key, node_key); let fullmesh = FullMeshPeeringStrategy::new(netapp.clone(), vec![], rpc_public_addr); + if let Some(ping_timeout) = config.rpc_ping_timeout_msec { + fullmesh.set_ping_timeout_millis(ping_timeout); + } let system_endpoint = netapp.endpoint(SYSTEM_RPC_PATH.into()); @@ -317,7 +319,13 @@ impl System { node_status: RwLock::new(HashMap::new()), netapp: netapp.clone(), fullmesh: fullmesh.clone(), - rpc: RpcHelper::new(netapp.id.into(), fullmesh, background.clone(), ring.clone()), + rpc: RpcHelper::new( + netapp.id.into(), + fullmesh, + background.clone(), + ring.clone(), + config.rpc_timeout_msec.map(Duration::from_millis), + ), system_endpoint, replication_factor, rpc_listen_addr: config.rpc_bind_addr, @@ -600,7 +608,7 @@ impl System { .broadcast( &self.system_endpoint, SystemRpc::AdvertiseStatus(local_status), - RequestStrategy::with_priority(PRIO_HIGH).with_timeout(SYSTEM_RPC_TIMEOUT), + RequestStrategy::with_priority(PRIO_HIGH), ) .await; @@ -724,7 +732,7 @@ impl System { &self.system_endpoint, peer, SystemRpc::PullClusterLayout, - RequestStrategy::with_priority(PRIO_HIGH).with_timeout(SYSTEM_RPC_TIMEOUT), + RequestStrategy::with_priority(PRIO_HIGH), ) .await; if let Ok(SystemRpc::AdvertiseClusterLayout(layout)) = resp { diff --git a/src/table/gc.rs b/src/table/gc.rs index 6cae9701..83e7eeff 100644 --- a/src/table/gc.rs +++ b/src/table/gc.rs @@ -25,8 +25,6 @@ use crate::replication::*; use crate::schema::*; const TABLE_GC_BATCH_SIZE: usize = 1024; -// Same timeout as NEED_BLOCK_QUERY_TIMEOUT in block manager -const TABLE_GC_RPC_TIMEOUT: Duration = Duration::from_secs(15); // GC delay for table entries: 1 day (24 hours) // (the delay before the entry is added in the GC todo list @@ -237,9 +235,7 @@ where &self.endpoint, &nodes[..], GcRpc::Update(updates), - RequestStrategy::with_priority(PRIO_BACKGROUND) - .with_quorum(nodes.len()) - .with_timeout(TABLE_GC_RPC_TIMEOUT), + RequestStrategy::with_priority(PRIO_BACKGROUND).with_quorum(nodes.len()), ) .await .err_context("GC: send tombstones")?; @@ -260,9 +256,7 @@ where &self.endpoint, &nodes[..], GcRpc::DeleteIfEqualHash(deletes), - RequestStrategy::with_priority(PRIO_BACKGROUND) - .with_quorum(nodes.len()) - .with_timeout(TABLE_GC_RPC_TIMEOUT), + RequestStrategy::with_priority(PRIO_BACKGROUND).with_quorum(nodes.len()), ) .await .err_context("GC: remote delete tombstones")?; diff --git a/src/table/sync.rs b/src/table/sync.rs index 62b88a58..76402d28 100644 --- a/src/table/sync.rs +++ b/src/table/sync.rs @@ -24,9 +24,6 @@ use crate::merkle::*; use crate::replication::*; use crate::*; -// Sync RPC can contain a lot of data, so have a 1min timeout -const TABLE_SYNC_RPC_TIMEOUT: Duration = Duration::from_secs(60); - // Do anti-entropy every 10 minutes const ANTI_ENTROPY_INTERVAL: Duration = Duration::from_secs(10 * 60); @@ -248,9 +245,7 @@ where &self.endpoint, nodes, SyncRpc::Items(values), - RequestStrategy::with_priority(PRIO_BACKGROUND) - .with_quorum(nodes.len()) - .with_timeout(TABLE_SYNC_RPC_TIMEOUT), + RequestStrategy::with_priority(PRIO_BACKGROUND).with_quorum(nodes.len()), ) .await?; @@ -311,8 +306,7 @@ where &self.endpoint, who, SyncRpc::RootCkHash(partition.partition, root_ck_hash), - RequestStrategy::with_priority(PRIO_BACKGROUND) - .with_timeout(TABLE_SYNC_RPC_TIMEOUT), + RequestStrategy::with_priority(PRIO_BACKGROUND), ) .await?; @@ -368,8 +362,7 @@ where &self.endpoint, who, SyncRpc::GetNode(key.clone()), - RequestStrategy::with_priority(PRIO_BACKGROUND) - .with_timeout(TABLE_SYNC_RPC_TIMEOUT), + RequestStrategy::with_priority(PRIO_BACKGROUND), ) .await? { @@ -445,8 +438,7 @@ where &self.endpoint, who, SyncRpc::Items(values), - RequestStrategy::with_priority(PRIO_BACKGROUND) - .with_timeout(TABLE_SYNC_RPC_TIMEOUT), + RequestStrategy::with_priority(PRIO_BACKGROUND), ) .await?; if let SyncRpc::Ok = rpc_resp { diff --git a/src/table/table.rs b/src/table/table.rs index 8e801be6..8a66c420 100644 --- a/src/table/table.rs +++ b/src/table/table.rs @@ -1,7 +1,6 @@ use std::borrow::Borrow; use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::sync::Arc; -use std::time::Duration; use async_trait::async_trait; use futures::stream::*; @@ -31,8 +30,6 @@ use crate::schema::*; use crate::sync::*; use crate::util::*; -pub const TABLE_RPC_TIMEOUT: Duration = Duration::from_secs(30); - pub struct Table { pub system: Arc, pub data: Arc>, @@ -124,8 +121,7 @@ where &who[..], rpc, RequestStrategy::with_priority(PRIO_NORMAL) - .with_quorum(self.data.replication.write_quorum()) - .with_timeout(TABLE_RPC_TIMEOUT), + .with_quorum(self.data.replication.write_quorum()), ) .await?; @@ -177,7 +173,7 @@ where &self.endpoint, node, rpc, - RequestStrategy::with_priority(PRIO_NORMAL).with_timeout(TABLE_RPC_TIMEOUT), + RequestStrategy::with_priority(PRIO_NORMAL), ) .await?; Ok::<_, Error>((node, resp)) @@ -234,7 +230,6 @@ where rpc, RequestStrategy::with_priority(PRIO_NORMAL) .with_quorum(self.data.replication.read_quorum()) - .with_timeout(TABLE_RPC_TIMEOUT) .interrupt_after_quorum(true), ) .await?; @@ -329,7 +324,6 @@ where rpc, RequestStrategy::with_priority(PRIO_NORMAL) .with_quorum(self.data.replication.read_quorum()) - .with_timeout(TABLE_RPC_TIMEOUT) .interrupt_after_quorum(true), ) .await?; @@ -406,9 +400,7 @@ where &self.endpoint, who, TableRpc::::Update(vec![what_enc]), - RequestStrategy::with_priority(PRIO_NORMAL) - .with_quorum(who.len()) - .with_timeout(TABLE_RPC_TIMEOUT), + RequestStrategy::with_priority(PRIO_NORMAL).with_quorum(who.len()), ) .await?; Ok(()) diff --git a/src/util/config.rs b/src/util/config.rs index 5e113e13..2d4b4f57 100644 --- a/src/util/config.rs +++ b/src/util/config.rs @@ -41,6 +41,11 @@ pub struct Config { /// Public IP address of this node pub rpc_public_addr: Option, + /// Timeout for Netapp's ping messagess + pub rpc_ping_timeout_msec: Option, + /// Timeout for Netapp RPC calls + pub rpc_timeout_msec: Option, + /// Bootstrap peers RPC address #[serde(default)] pub bootstrap_peers: Vec, From 1f7b050b7dd975642c8cd5d8a7562d347cfa528d Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 20 Sep 2022 11:49:48 +0200 Subject: [PATCH 128/149] Change a warn! into a debug! --- Cargo.lock | 1 + Cargo.nix | 13 +++++++------ src/table/Cargo.toml | 1 + src/table/sync.rs | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 99ba4d3a..a852245d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1179,6 +1179,7 @@ dependencies = [ "garage_db", "garage_rpc", "garage_util", + "hex", "hexdump", "opentelemetry", "rand 0.8.5", diff --git a/Cargo.nix b/Cargo.nix index 0a472f2a..b71335ac 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -780,7 +780,7 @@ in registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; sha256 = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"; }; dependencies = { - ${ if hostPlatform.config == "aarch64-linux-android" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-linux-android" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; }; }); @@ -1667,6 +1667,7 @@ in garage_db = rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }; garage_rpc = rustPackages."unknown".garage_rpc."0.8.0" { inherit profileName; }; garage_util = rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }; + hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }; hexdump = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hexdump."0.1.1" { inherit profileName; }; opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }; rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }; @@ -3887,7 +3888,7 @@ in ]; dependencies = { ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "dragonfly" || hostPlatform.parsed.kernel.name == "freebsd" || hostPlatform.parsed.kernel.name == "illumos" || hostPlatform.parsed.kernel.name == "netbsd" || hostPlatform.parsed.kernel.name == "openbsd" || hostPlatform.parsed.kernel.name == "solaris" || hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "once_cell" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "i686" || hostPlatform.parsed.cpu.name == "x86_64" || (hostPlatform.parsed.cpu.name == "aarch64" || hostPlatform.parsed.cpu.name == "armv6l" || hostPlatform.parsed.cpu.name == "armv7l") && (hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "fuchsia" || hostPlatform.parsed.kernel.name == "linux") then "spin" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".spin."0.5.2" { inherit profileName; }; untrusted = rustPackages."registry+https://github.com/rust-lang/crates.io-index".untrusted."0.7.1" { inherit profileName; }; ${ if hostPlatform.parsed.cpu.name == "wasm32" && hostPlatform.parsed.vendor.name == "unknown" && hostPlatform.parsed.kernel.name == "unknown" && hostPlatform.parsed.abi.name == "" then "web_sys" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.56" { inherit profileName; }; @@ -4497,7 +4498,7 @@ in ]; dependencies = { bitflags = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }; - ${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; + ${ if hostPlatform.parsed.kernel.name == "android" || hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; }; ${ if !(hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android") then "parking_lot_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot_core."0.8.5" { inherit profileName; }; static_init_macro = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init_macro."1.0.2" { profileName = "__noProfile"; }; @@ -5597,11 +5598,11 @@ in [ "default" ] ]; dependencies = { - ${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "aarch64-pc-windows-msvc" || hostPlatform.config == "aarch64-uwp-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "i686-pc-windows-msvc" || hostPlatform.config == "i686-uwp-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; }; ${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; }; - ${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; + ${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; }; }; }); diff --git a/src/table/Cargo.toml b/src/table/Cargo.toml index ae52e8d7..38c6b41c 100644 --- a/src/table/Cargo.toml +++ b/src/table/Cargo.toml @@ -22,6 +22,7 @@ opentelemetry = "0.17" async-trait = "0.1.7" bytes = "1.0" +hex = "0.4" hexdump = "0.1" tracing = "0.1.30" rand = "0.8" diff --git a/src/table/sync.rs b/src/table/sync.rs index 62b88a58..28e99dd3 100644 --- a/src/table/sync.rs +++ b/src/table/sync.rs @@ -351,11 +351,11 @@ where // Just send that item directly if let Some(val) = self.data.store.get(&ik[..])? { if blake2sum(&val[..]) != ivhash { - warn!("({}) Hashes differ between stored value and Merkle tree, key: {:?} (if your server is very busy, don't worry, this happens when the Merkle tree can't be updated fast enough)", F::TABLE_NAME, ik); + debug!("({}) Hashes differ between stored value and Merkle tree, key: {} (if your server is very busy, don't worry, this happens when the Merkle tree can't be updated fast enough)", F::TABLE_NAME, hex::encode(ik)); } todo_items.push(val.to_vec()); } else { - warn!("({}) Item from Merkle tree not found in store: {:?} (if your server is very busy, don't worry, this happens when the Merkle tree can't be updated fast enough)", F::TABLE_NAME, ik); + debug!("({}) Item from Merkle tree not found in store: {} (if your server is very busy, don't worry, this happens when the Merkle tree can't be updated fast enough)", F::TABLE_NAME, hex::encode(ik)); } } MerkleNode::Intermediate(l) => { From ded444f6c96f8ab991e762f65760b42e4d64246c Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 20 Sep 2022 16:01:41 +0200 Subject: [PATCH 129/149] Ability to have custom timeouts in request strategy (not used) --- src/model/k2v/causality.rs | 2 +- src/model/k2v/item_table.rs | 8 ++++---- src/rpc/rpc_helper.rs | 30 +++++++++++++++++++++--------- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/model/k2v/causality.rs b/src/model/k2v/causality.rs index 8c76a32b..9a692870 100644 --- a/src/model/k2v/causality.rs +++ b/src/model/k2v/causality.rs @@ -15,7 +15,7 @@ pub fn make_node_id(node_id: Uuid) -> K2VNodeId { u64::from_be_bytes(tmp) } -#[derive(PartialEq, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)] pub struct CausalContext { pub vector_clock: BTreeMap, } diff --git a/src/model/k2v/item_table.rs b/src/model/k2v/item_table.rs index baa1db4b..7860cb17 100644 --- a/src/model/k2v/item_table.rs +++ b/src/model/k2v/item_table.rs @@ -17,7 +17,7 @@ pub const CONFLICTS: &str = "conflicts"; pub const VALUES: &str = "values"; pub const BYTES: &str = "bytes"; -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct K2VItem { pub partition: K2VItemPartition, pub sort_key: String, @@ -25,19 +25,19 @@ pub struct K2VItem { items: BTreeMap, } -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash, Eq)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, Hash)] pub struct K2VItemPartition { pub bucket_id: Uuid, pub partition_key: String, } -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] struct DvvsEntry { t_discard: u64, values: Vec<(u64, DvvsValue)>, } -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub enum DvvsValue { Value(#[serde(with = "serde_bytes")] Vec), Deleted, diff --git a/src/rpc/rpc_helper.rs b/src/rpc/rpc_helper.rs index 857ed620..949aced6 100644 --- a/src/rpc/rpc_helper.rs +++ b/src/rpc/rpc_helper.rs @@ -44,8 +44,15 @@ pub struct RequestStrategy { pub rs_interrupt_after_quorum: bool, /// Request priority pub rs_priority: RequestPriority, - /// Deactivate timeout for this request - pub rs_no_timeout: bool, + /// Custom timeout for this request + rs_timeout: Timeout, +} + +#[derive(Copy, Clone)] +enum Timeout { + None, + Default, + Custom(Duration), } impl RequestStrategy { @@ -55,7 +62,7 @@ impl RequestStrategy { rs_quorum: None, rs_interrupt_after_quorum: false, rs_priority: prio, - rs_no_timeout: false, + rs_timeout: Timeout::Default, } } /// Set quorum to be reached for request @@ -71,7 +78,12 @@ impl RequestStrategy { } /// Deactivate timeout for this request pub fn without_timeout(mut self) -> Self { - self.rs_no_timeout = true; + self.rs_timeout = Timeout::None; + self + } + /// Set custom timeout for this request + pub fn with_custom_timeout(mut self, timeout: Duration) -> Self { + self.rs_timeout = Timeout::Custom(timeout); self } } @@ -138,10 +150,10 @@ impl RpcHelper { .record_duration(&self.0.metrics.rpc_duration, &metric_tags); let timeout = async { - if strat.rs_no_timeout { - futures::future::pending().await - } else { - tokio::time::sleep(self.0.rpc_timeout).await + match strat.rs_timeout { + Timeout::None => futures::future::pending().await, + Timeout::Default => tokio::time::sleep(self.0.rpc_timeout).await, + Timeout::Custom(t) => tokio::time::sleep(t).await, } }; @@ -412,7 +424,7 @@ impl RpcHelper { .iter() .find(|x| x.id.as_ref() == to.as_slice()) .and_then(|pi| pi.avg_ping) - .unwrap_or_else(|| Duration::from_secs(1)); + .unwrap_or_else(|| Duration::from_secs(10)); ( *to != self.0.our_node_id, peer_zone != our_zone, From 782630fc27b41b9ae58d1417cace2995c99856fc Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 20 Sep 2022 17:45:18 +0200 Subject: [PATCH 130/149] Initialize metrics exporter earlier (fix #389) --- nix/compile.nix | 4 ++-- src/api/admin/api_server.rs | 7 +++++-- src/garage/server.rs | 9 ++++++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/nix/compile.nix b/nix/compile.nix index 512a7354..4382a2be 100644 --- a/nix/compile.nix +++ b/nix/compile.nix @@ -137,8 +137,8 @@ let /* [2] */ hardeningDisable = [ "pie" ]; }; overrideArgs = old: { - /* [4] */ features = [ "bundled-libs" "sled" ] - ++ (if release then [ "kubernetes-discovery" "telemetry-otlp" "metrics" "lmdb" "sqlite" ] else []); + /* [4] */ features = [ "bundled-libs" "sled" "metrics" ] + ++ (if release then [ "kubernetes-discovery" "telemetry-otlp" "lmdb" "sqlite" ] else []); }; }) diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs index fb0078cc..0816bda1 100644 --- a/src/api/admin/api_server.rs +++ b/src/api/admin/api_server.rs @@ -34,7 +34,10 @@ pub struct AdminApiServer { } impl AdminApiServer { - pub fn new(garage: Arc) -> Self { + pub fn new( + garage: Arc, + #[cfg(feature = "metrics")] exporter: PrometheusExporter, + ) -> Self { let cfg = &garage.config.admin; let metrics_token = cfg .metrics_token @@ -47,7 +50,7 @@ impl AdminApiServer { Self { garage, #[cfg(feature = "metrics")] - exporter: opentelemetry_prometheus::exporter().init(), + exporter, metrics_token, admin_token, } diff --git a/src/garage/server.rs b/src/garage/server.rs index aeef02a2..28710a8e 100644 --- a/src/garage/server.rs +++ b/src/garage/server.rs @@ -32,6 +32,9 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> { // ---- Initialize Garage internals ---- + #[cfg(feature = "metrics")] + let metrics_exporter = opentelemetry_prometheus::exporter().init(); + info!("Initializing background runner..."); let watch_cancel = netapp::util::watch_ctrl_c(); let (background, await_background_done) = BackgroundRunner::new(16, watch_cancel.clone()); @@ -50,7 +53,11 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> { } info!("Initialize Admin API server and metrics collector..."); - let admin_server = AdminApiServer::new(garage.clone()); + let admin_server = AdminApiServer::new( + garage.clone(), + #[cfg(feature = "metrics")] + metrics_exporter, + ); info!("Launching internal Garage cluster communications..."); let run_system = tokio::spawn(garage.system.clone().run(watch_cancel.clone())); From e89f8806949f4b389f8848454e293b7b5ba6d91a Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 20 Sep 2022 17:54:41 +0200 Subject: [PATCH 131/149] Enable k2v feature flag in CI --- nix/compile.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/compile.nix b/nix/compile.nix index 4382a2be..adb07886 100644 --- a/nix/compile.nix +++ b/nix/compile.nix @@ -137,7 +137,7 @@ let /* [2] */ hardeningDisable = [ "pie" ]; }; overrideArgs = old: { - /* [4] */ features = [ "bundled-libs" "sled" "metrics" ] + /* [4] */ features = [ "bundled-libs" "sled" "metrics" "k2v" ] ++ (if release then [ "kubernetes-discovery" "telemetry-otlp" "lmdb" "sqlite" ] else []); }; }) From a3758dc4c4a18b11d5dd30e0136d9339c005ac31 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 21 Sep 2022 12:40:55 +0200 Subject: [PATCH 132/149] Update README --- README.md | 28 +++++++++++++++++----------- doc/book/design/goals.md | 2 +- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index ec7e2485..9992fff2 100644 --- a/README.md +++ b/README.md @@ -15,18 +15,24 @@ Garage [![Build Status](https://drone.deuxfleurs.fr/api/badges/Deuxfleurs/garage ]

      -Garage is a lightweight S3-compatible distributed object store, with the following goals: +Garage is an S3-compatible distributed object storage service +designed for self-hosting at a small-to-medium scale. -- As self-contained as possible -- Easy to set up -- Highly resilient to network failures, network latency, disk failures, sysadmin failures -- Relatively simple -- Made for multi-datacenter deployments +Garage is designed for storage clusters composed of nodes running +at different physical locations, +in order to easily provide a storage service that replicates data at these different +locations and stays available even when some servers are unreachable. +Garage also focuses on being lightweight, easy to operate, and highly resilient to +machine failures. -Non-goals include: +Garage is built by [Deuxfleurs](https://deuxfleurs.fr), +an experimental small-scale self hosted service provider, +which has been using it in production since its first release in 2020. -- Extremely high performance -- Complete implementation of the S3 API -- Erasure coding (our replication model is simply to copy the data as is on several nodes, in different datacenters if possible) +Learn more on our dedicated documentation pages: -Our main use case is to provide a distributed storage layer for small-scale self hosted services such as [Deuxfleurs](https://deuxfleurs.fr). +- [Goals and use cases](https://garagehq.deuxfleurs.fr/documentation/design/goals/) +- [Features](https://garagehq.deuxfleurs.fr/documentation/reference-manual/features/) +- [Quick start](https://garagehq.deuxfleurs.fr/documentation/quick-start/) + +Garage is entirely free software released under the terms of the AGPLv3. diff --git a/doc/book/design/goals.md b/doc/book/design/goals.md index b97d73a9..9c2d89f0 100644 --- a/doc/book/design/goals.md +++ b/doc/book/design/goals.md @@ -14,10 +14,10 @@ website. Garage is an opinionated object storage solutoin, we focus on the following **desirable properties**: + - **Internet enabled**: made for multi-sites (eg. datacenters, offices, households, etc.) interconnected through regular Internet connections. - **Self-contained & lightweight**: works everywhere and integrates well in existing environments to target [hyperconverged infrastructures](https://en.wikipedia.org/wiki/Hyper-converged_infrastructure). - **Highly resilient**: highly resilient to network failures, network latency, disk failures, sysadmin failures. - **Simple**: simple to understand, simple to operate, simple to debug. - - **Internet enabled**: made for multi-sites (eg. datacenters, offices, households, etc.) interconnected through regular Internet connections. We also noted that the pursuit of some other goals are detrimental to our initial goals. The following has been identified as **non-goals** (if these points matter to you, you should not use Garage): From 1778e4b3187af979cd67098e555455be422c9500 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 26 Sep 2022 16:20:30 +0200 Subject: [PATCH 133/149] Fix span name for api server requests --- src/api/generic_server.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/api/generic_server.rs b/src/api/generic_server.rs index a48be1bc..62fe4e5a 100644 --- a/src/api/generic_server.rs +++ b/src/api/generic_server.rs @@ -174,7 +174,11 @@ impl ApiServer { let current_context = Context::current(); let current_span = current_context.span(); - current_span.update_name::(format!("S3 API {}", endpoint.name())); + current_span.update_name::(format!( + "{} API {}", + A::API_NAME_DISPLAY, + endpoint.name() + )); current_span.set_attribute(KeyValue::new("endpoint", endpoint.name())); endpoint.add_span_attributes(current_span); From 69bcc813de462a8b13388d11b491146c937b8d9a Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 26 Sep 2022 17:41:38 +0200 Subject: [PATCH 134/149] Add garage v0.8 migration guide --- doc/book/working-documents/migration-07.md | 2 +- doc/book/working-documents/migration-08.md | 34 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 doc/book/working-documents/migration-08.md diff --git a/doc/book/working-documents/migration-07.md b/doc/book/working-documents/migration-07.md index 2d0444db..03cdfedc 100644 --- a/doc/book/working-documents/migration-07.md +++ b/doc/book/working-documents/migration-07.md @@ -16,7 +16,7 @@ The migration steps are as follows: 1. Do `garage repair --all-nodes --yes tables` and `garage repair --all-nodes --yes blocks`, check the logs and check that all data seems to be synced correctly between nodes. If you have time, do additional checks (`scrub`, `block_refs`, etc.) -2. Disable api and web access. Garage does not support disabling +2. Disable API and web access. Garage does not support disabling these endpoints but you can change the port number or stop your reverse proxy for instance. 3. Check once again that your cluster is healty. Run again `garage repair --all-nodes --yes tables` which is quick. diff --git a/doc/book/working-documents/migration-08.md b/doc/book/working-documents/migration-08.md new file mode 100644 index 00000000..5f97c45b --- /dev/null +++ b/doc/book/working-documents/migration-08.md @@ -0,0 +1,34 @@ ++++ +title = "Migrating from 0.7 to 0.8" +weight = 13 ++++ + +**This guide explains how to migrate to 0.8 if you have an existing 0.7 cluster. +We don't recommend trying to migrate to 0.8 directly from 0.6 or older.** + +**We make no guarantee that this migration will work perfectly: +back up all your data before attempting it!** + +Garage v0.8 introduces new data tables that allow the counting of objects in buckets in order to implement bucket quotas. +A manual migration step is required to first count objects in Garage buckets and populate these tables with accurate data. + +The migration steps are as follows: + +1. Disable API and web access. Garage v0.7 does not support disabling + these endpoints but you can change the port number or stop your reverse proxy for instance. +2. Do `garage repair --all-nodes --yes tables` and `garage repair --all-nodes --yes blocks`, + check the logs and check that all data seems to be synced correctly between + nodes. If you have time, do additional checks (`scrub`, `block_refs`, etc.) +3. Check that queues are empty: run `garage stats` to query them or inspect metrics in the Grafana dashboard. +4. Turn off Garage v0.7 +5. **Backup the metadata folder of all your nodes!** For instance, use the following command + if your metadata directory is `/var/lib/garage/meta`: `cd /var/lib/garage ; tar -acf meta-v0.7.tar.zst meta/` +6. Install Garage v0.8 +7. **Before starting Garage v0.8**, run the offline migration step: `garage offline-repair --yes object_counters`. + This can take a while to run, depending on the number of objects stored in your cluster. +8. Turn on Garage v0.8 +9. Do `garage repair --all-nodes --yes tables` and `garage repair --all-nodes --yes blocks`. + Wait for a full table sync to run. +10. Your upgraded cluster should be in a working state. Re-enable API and Web + access and check that everything went well. +11. Monitor your cluster in the next hours to see if it works well under your production load, report any issue. From 194e8be1bbed076ca811176123cc07abb4a8d04d Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 26 Sep 2022 18:01:17 +0200 Subject: [PATCH 135/149] Update docker image links --- doc/book/cookbook/real-world.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/book/cookbook/real-world.md b/doc/book/cookbook/real-world.md index e101a706..4fcb5cf7 100644 --- a/doc/book/cookbook/real-world.md +++ b/doc/book/cookbook/real-world.md @@ -51,15 +51,15 @@ to store 2 TB of data in total. ## Get a Docker image -Our docker image is currently named `dxflrs/amd64_garage` and is stored on the [Docker Hub](https://hub.docker.com/r/dxflrs/amd64_garage/tags?page=1&ordering=last_updated). -We encourage you to use a fixed tag (eg. `v0.4.0`) and not the `latest` tag. -For this example, we will use the latest published version at the time of the writing which is `v0.4.0` but it's up to you -to check [the most recent versions on the Docker Hub](https://hub.docker.com/r/dxflrs/amd64_garage/tags?page=1&ordering=last_updated). +Our docker image is currently named `dxflrs/garage` and is stored on the [Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated). +We encourage you to use a fixed tag (eg. `v0.8.0`) and not the `latest` tag. +For this example, we will use the latest published version at the time of the writing which is `v0.8.0` but it's up to you +to check [the most recent versions on the Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated). For example: ``` -sudo docker pull dxflrs/amd64_garage:v0.4.0 +sudo docker pull dxflrs/garage:v0.8.0 ``` ## Deploying and configuring Garage @@ -125,7 +125,7 @@ docker run \ -v /etc/garage.toml:/etc/garage.toml \ -v /var/lib/garage/meta:/var/lib/garage/meta \ -v /var/lib/garage/data:/var/lib/garage/data \ - lxpz/garage_amd64:v0.4.0 + dxflrs/garage:v0.8.0 ``` It should be restarted automatically at each reboot. From d104ae871170e5d1dac644921815bcce8496f23c Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 26 Sep 2022 17:19:21 +0200 Subject: [PATCH 136/149] Add step to generate multi-arch Docker container in CI --- .drone.yml | 13 ++++++++++++- nix/manifest-tool.nix | 23 +++++++++++++++++++++++ shell.nix | 30 ++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 nix/manifest-tool.nix diff --git a/.drone.yml b/.drone.yml index 277bfbc2..f5ac6179 100644 --- a/.drone.yml +++ b/.drone.yml @@ -245,6 +245,17 @@ node: nix-daemon: 1 steps: + - name: multiarch-docker + image: nixpkgs/nix:nixos-22.05 + environment: + DOCKER_AUTH: + from_secret: docker_auth + HOME: "/root" + commands: + - mkdir -p /root/.docker + - echo $DOCKER_AUTH > /root/.docker/config.json + - export CONTAINER_TAG=${DRONE_TAG:-$DRONE_COMMIT} + - nix-shell --attr release --run "multiarch_docker" - name: refresh-index image: nixpkgs/nix:nixos-22.05 environment: @@ -269,6 +280,6 @@ trigger: --- kind: signature -hmac: 362639b4c9541ad9bd06ff7f72b5235b2b0216bcb16eececd25285b6fe94ba6f +hmac: 103a04785c98f5376a63ce22865c2576963019bbc4d828f200d2a470a3c821ea ... diff --git a/nix/manifest-tool.nix b/nix/manifest-tool.nix new file mode 100644 index 00000000..182ccc0e --- /dev/null +++ b/nix/manifest-tool.nix @@ -0,0 +1,23 @@ +pkgs: +pkgs.buildGoModule rec { + pname = "manifest-tool"; + version = "2.0.5"; + + src = pkgs.fetchFromGitHub { + owner = "estesp"; + repo = "manifest-tool"; + rev = "v${version}"; + sha256 = "hjCGKnE0yrlnF/VIzOwcDzmQX3Wft+21KCny/opqdLg="; + } + "/v2"; + + vendorSha256 = null; + + checkPhase = "true"; + + meta = with pkgs.lib; { + description = "Command line tool to create and query container image manifest list/indexes"; + homepage = "https://github.com/estesp/manifest-tool"; + license = licenses.asl20; + platforms = platforms.linux; + }; +} diff --git a/shell.nix b/shell.nix index ea5d6356..e82bce78 100644 --- a/shell.nix +++ b/shell.nix @@ -10,6 +10,7 @@ let overlays = [ cargo2nixOverlay ]; }; kaniko = (import ./nix/kaniko.nix) pkgs; + manifest-tool = (import ./nix/manifest-tool.nix) pkgs; winscp = (import ./nix/winscp.nix) pkgs; in @@ -84,6 +85,34 @@ function to_docker { --verbosity=debug } +function multiarch_docker { + manifest-tool push from-spec <(cat < Date: Tue, 27 Sep 2022 16:52:36 +0200 Subject: [PATCH 137/149] Document db_engine --- doc/book/reference-manual/configuration.md | 43 ++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/doc/book/reference-manual/configuration.md b/doc/book/reference-manual/configuration.md index 6db12568..97da0e0e 100644 --- a/doc/book/reference-manual/configuration.md +++ b/doc/book/reference-manual/configuration.md @@ -9,6 +9,8 @@ Here is an example `garage.toml` configuration file that illustrates all of the metadata_dir = "/var/lib/garage/meta" data_dir = "/var/lib/garage/data" +db_engine = "lmdb" + block_size = 1048576 replication_mode = "3" @@ -71,6 +73,47 @@ This folder can be placed on an HDD. The space available for `data_dir` should be counted to determine a node's capacity when [adding it to the cluster layout](@/documentation/cookbook/real-world.md). +### `db_engine` (since `v0.8.0`) + +By default, Garage uses the Sled embedded database library +to store its metadata on-disk. Since `v0.8.0`, Garage can use alternative storage backends as follows: + +| DB engine | `db_engine` value | Database path | +| --------- | ----------------- | ------------- | +| [Sled](https://sled.rs) | `"sled"` | `/db/` | +| [LMDB](https://www.lmdb.tech) | `"lmdb"` | `/db.lmdb/` | +| [Sqlite](https://sqlite.org) | `"sqlite"` | `/db.sqlite` | + +Performance characteristics of the different DB engines are as follows: + +- Sled: the default database engine, which tends to produce + large data files and also has performance issues, especially when the metadata folder + is on a traditionnal HDD and not on SSD. +- LMDB: the recommended alternative on 64-bit systems, + much more space-efficiant and slightly faster. Note that the data format of LMDB is not portable + between architectures, so for instance the Garage database of an x86-64 + node cannot be moved to an ARM64 node. Also note that, while LMDB can technically be used on 32-bit systems, + this will limit your node to very small database sizes due to how LMDB works; it is therefore not recommended. +- Sqlite: Garage supports Sqlite as a storage backend for metadata, + however it may have issues and is also very slow in its current implementation, + so it is not recommended to be used for now. + +It is possible to convert Garage's metadata directory from one format to another with a small utility named `convert_db`, +which can be downloaded at the following locations: +[for amd64](https://garagehq.deuxfleurs.fr/_releases/convert_db/amd64/convert_db), +[for i386](https://garagehq.deuxfleurs.fr/_releases/convert_db/i386/convert_db), +[for arm64](https://garagehq.deuxfleurs.fr/_releases/convert_db/arm64/convert_db), +[for arm](https://garagehq.deuxfleurs.fr/_releases/convert_db/arm/convert_db). +The `convert_db` utility is used as folows: + +``` +convert-db -a -i \ + -b -o +``` + +Make sure to specify the full database path as presented in the table above, +and not just the path to the metadata directory. + ### `block_size` Garage splits stored objects in consecutive chunks of size `block_size` From 1f97ce37e682dff13472d6402f6115e8c1bbb0d7 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 28 Sep 2022 10:41:59 +0200 Subject: [PATCH 138/149] Shutdown properly on SIGTERM/SIGHUP and on Windows signals --- src/garage/server.rs | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/garage/server.rs b/src/garage/server.rs index 28710a8e..d4099a97 100644 --- a/src/garage/server.rs +++ b/src/garage/server.rs @@ -36,7 +36,7 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> { let metrics_exporter = opentelemetry_prometheus::exporter().init(); info!("Initializing background runner..."); - let watch_cancel = netapp::util::watch_ctrl_c(); + let watch_cancel = watch_shutdown_signal(); let (background, await_background_done) = BackgroundRunner::new(16, watch_cancel.clone()); info!("Initializing Garage main data store..."); @@ -157,3 +157,44 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> { Ok(()) } + +#[cfg(unix)] +fn watch_shutdown_signal() -> watch::Receiver { + use tokio::signal::unix::*; + + let (send_cancel, watch_cancel) = watch::channel(false); + tokio::spawn(async move { + let mut sigint = signal(SignalKind::interrupt()).expect("Failed to install SIGINT handler"); + let mut sigterm = + signal(SignalKind::terminate()).expect("Failed to install SIGTERM handler"); + let mut sighup = signal(SignalKind::hangup()).expect("Failed to install SIGHUP handler"); + tokio::select! { + _ = sigint.recv() => info!("Received SIGINT, shutting down."), + _ = sigterm.recv() => info!("Received SIGTERM, shutting down."), + _ = sighup.recv() => info!("Received SIGHUP, shutting down."), + } + send_cancel.send(true).unwrap(); + }); + watch_cancel +} + +#[cfg(windows)] +fn watch_shutdown_signal() -> watch::Receiver { + use tokio::signal::windows::*; + + let (send_cancel, watch_cancel) = watch::channel(false); + tokio::spawn(async move { + let mut sigint = ctrl_c().expect("Failed to install Ctrl-C handler"); + let mut sigclose = ctrl_close().expect("Failed to install Ctrl-Close handler"); + let mut siglogoff = ctrl_logoff().expect("Failed to install Ctrl-Logoff handler"); + let mut sigsdown = ctrl_shutdown().expect("Failed to install Ctrl-Shutdown handler"); + tokio::select! { + _ = sigint.recv() => info!("Received Ctrl-C, shutting down."), + _ = sigclose.recv() => info!("Received Ctrl-Close, shutting down."), + _ = siglogoff.recv() => info!("Received Ctrl-Logoff, shutting down."), + _ = sigsdown.recv() => info!("Received Ctrl-Shutdown, shutting down."), + } + send_cancel.send(true).unwrap(); + }); + watch_cancel +} From ad917ffd3f76316e48b89ff17e2f8a600a269481 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Thu, 29 Sep 2022 15:53:54 +0200 Subject: [PATCH 139/149] Fix instant substractions that might have panicked --- src/rpc/system.rs | 4 +++- src/table/sync.rs | 2 +- src/util/metrics.rs | 16 +++++++++++----- src/util/tranquilizer.rs | 2 +- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/rpc/system.rs b/src/rpc/system.rs index f8121193..9e0bfa11 100644 --- a/src/rpc/system.rs +++ b/src/rpc/system.rs @@ -369,7 +369,9 @@ impl System { id: n.id.into(), addr: n.addr, is_up: n.is_up(), - last_seen_secs_ago: n.last_seen.map(|t| (Instant::now() - t).as_secs()), + last_seen_secs_ago: n + .last_seen + .map(|t| (Instant::now().saturating_duration_since(t)).as_secs()), status: node_status .get(&n.id.into()) .cloned() diff --git a/src/table/sync.rs b/src/table/sync.rs index e34aa8d7..9d79d856 100644 --- a/src/table/sync.rs +++ b/src/table/sync.rs @@ -607,7 +607,7 @@ impl Worker for SyncWor self.add_full_sync(); } }, - _ = tokio::time::sleep(self.next_full_sync - Instant::now()) => { + _ = tokio::time::sleep_until(self.next_full_sync.into()) => { self.add_full_sync(); } } diff --git a/src/util/metrics.rs b/src/util/metrics.rs index 1b05eabe..b882a886 100644 --- a/src/util/metrics.rs +++ b/src/util/metrics.rs @@ -1,4 +1,4 @@ -use std::time::SystemTime; +use std::time::Instant; use futures::{future::BoxFuture, Future, FutureExt}; use rand::Rng; @@ -28,10 +28,12 @@ where attributes: &'a [KeyValue], ) -> BoxFuture<'a, Self::Output> { async move { - let request_start = SystemTime::now(); + let request_start = Instant::now(); let res = self.await; r.record( - request_start.elapsed().map_or(0.0, |d| d.as_secs_f64()), + Instant::now() + .saturating_duration_since(request_start) + .as_secs_f64(), attributes, ); res @@ -41,9 +43,13 @@ where fn bound_record_duration(self, r: &'a BoundValueRecorder) -> BoxFuture<'a, Self::Output> { async move { - let request_start = SystemTime::now(); + let request_start = Instant::now(); let res = self.await; - r.record(request_start.elapsed().map_or(0.0, |d| d.as_secs_f64())); + r.record( + Instant::now() + .saturating_duration_since(request_start) + .as_secs_f64(), + ); res } .boxed() diff --git a/src/util/tranquilizer.rs b/src/util/tranquilizer.rs index fdb2918b..8a96cbb3 100644 --- a/src/util/tranquilizer.rs +++ b/src/util/tranquilizer.rs @@ -36,7 +36,7 @@ impl Tranquilizer { } fn tranquilize_internal(&mut self, tranquility: u32) -> Option { - let observation = Instant::now() - self.last_step_begin; + let observation = Instant::now().saturating_duration_since(self.last_step_begin); self.observations.push_back(observation); self.sum_observations += observation; From a93dcce84196bb8ffc8cef091d1343597b15b9a6 Mon Sep 17 00:00:00 2001 From: chemicstry Date: Mon, 20 Jun 2022 15:52:43 +0300 Subject: [PATCH 140/149] Add helm chart --- script/helm/README.md | 63 +++++++++ script/helm/garage/.helmignore | 23 ++++ script/helm/garage/Chart.yaml | 24 ++++ script/helm/garage/templates/_helpers.tpl | 62 +++++++++ script/helm/garage/templates/configmap.yaml | 29 ++++ script/helm/garage/templates/ingress.yaml | 123 +++++++++++++++++ script/helm/garage/templates/service.yaml | 19 +++ .../helm/garage/templates/serviceaccount.yaml | 12 ++ script/helm/garage/templates/statefulset.yaml | 97 ++++++++++++++ script/helm/garage/values.yaml | 124 ++++++++++++++++++ 10 files changed, 576 insertions(+) create mode 100644 script/helm/README.md create mode 100644 script/helm/garage/.helmignore create mode 100644 script/helm/garage/Chart.yaml create mode 100644 script/helm/garage/templates/_helpers.tpl create mode 100644 script/helm/garage/templates/configmap.yaml create mode 100644 script/helm/garage/templates/ingress.yaml create mode 100644 script/helm/garage/templates/service.yaml create mode 100644 script/helm/garage/templates/serviceaccount.yaml create mode 100644 script/helm/garage/templates/statefulset.yaml create mode 100644 script/helm/garage/values.yaml diff --git a/script/helm/README.md b/script/helm/README.md new file mode 100644 index 00000000..715cbab1 --- /dev/null +++ b/script/helm/README.md @@ -0,0 +1,63 @@ +# Garage helm3 chart + +This chart deploys garage on a kubernetes cluster. + +## Deploying + +With default options: + +```bash +helm install --create-namespace --namespace garage garage ./garage +``` + +With custom values: + +```bash +helm install --create-namespace --namespace garage garage ./garage -f values.override.yaml +``` + +## Overriding default values + +All possible configuration values can be found in [values.yaml](garage/values.yaml). + +This is an example `values.overrride.yaml` for deploying in a microk8s cluster with a https s3 api ingress route: + +```yaml +# Start 4 instances (StatefulSets) of garage +replicaCount: 4 + +# Override default storage class and size +persistence: + meta: + storageClass: "openebs-hostpath" + size: 100Mi + data: + storageClass: "openebs-hostpath" + size: 1Gi + +ingress: + s3: + api: + enabled: true + className: "public" + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" + nginx.ingress.kubernetes.io/proxy-body-size: 500m + hosts: + - host: s3-api.my-domain.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: garage-ingress-cert + hosts: + - s3-api.my-domain.com +``` + +## Removing + +```bash +helm delete --namespace garage garage +``` + +Note that this will leave behind custom CRD `garagenodes.deuxfleurs.fr`, which must be removed manually if desired. diff --git a/script/helm/garage/.helmignore b/script/helm/garage/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/script/helm/garage/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/script/helm/garage/Chart.yaml b/script/helm/garage/Chart.yaml new file mode 100644 index 00000000..9455488a --- /dev/null +++ b/script/helm/garage/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: garage +description: S3-compatible object store for small self-hosted geo-distributed deployments + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "v0.7.2" diff --git a/script/helm/garage/templates/_helpers.tpl b/script/helm/garage/templates/_helpers.tpl new file mode 100644 index 00000000..1a651f47 --- /dev/null +++ b/script/helm/garage/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "garage.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "garage.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "garage.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "garage.labels" -}} +helm.sh/chart: {{ include "garage.chart" . }} +{{ include "garage.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "garage.selectorLabels" -}} +app.kubernetes.io/name: {{ include "garage.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "garage.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "garage.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/script/helm/garage/templates/configmap.yaml b/script/helm/garage/templates/configmap.yaml new file mode 100644 index 00000000..587746f6 --- /dev/null +++ b/script/helm/garage/templates/configmap.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "garage.fullname" . }}-config +data: + garage.toml: |- + metadata_dir = "{{ .Values.garage.metadataDir }}" + data_dir = "{{ .Values.garage.dataDir }}" + + replication_mode = "{{ .Values.garage.replicationMode }}" + + rpc_bind_addr = "{{ .Values.garage.rpcBindAddr }}" + rpc_secret = "{{ .Values.garage.rpcSecret }}" + + bootstrap_peers = {{ .Values.garage.bootstrapPeers }} + + kubernetes_namespace = "{{ .Release.Namespace }}" + kubernetes_service_name = "{{ include "garage.fullname" . }}" + kubernetes_skip_crd = {{ .Values.garage.kubernetesSkipCrd }} + + [s3_api] + s3_region = "{{ .Values.garage.s3.api.region }}" + api_bind_addr = "[::]:3900" + root_domain = "{{ .Values.garage.s3.api.rootDomain }}" + + [s3_web] + bind_addr = "[::]:3902" + root_domain = "{{ .Values.garage.s3.web.rootDomain }}" + index = "{{ .Values.garage.s3.web.index }}" \ No newline at end of file diff --git a/script/helm/garage/templates/ingress.yaml b/script/helm/garage/templates/ingress.yaml new file mode 100644 index 00000000..c4ee5a3f --- /dev/null +++ b/script/helm/garage/templates/ingress.yaml @@ -0,0 +1,123 @@ +{{- if .Values.ingress.s3.api.enabled -}} +{{- $fullName := include "garage.fullname" . -}} +{{- $svcPort := .Values.service.s3.api.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.s3.api.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.s3.api.annotations "kubernetes.io/ingress.class" .Values.ingress.s3.api.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }}-s3-api + labels: + {{- include "garage.labels" . | nindent 4 }} + {{- with .Values.ingress.s3.api.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.s3.api.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.s3.api.className }} + {{- end }} + {{- if .Values.ingress.s3.api.tls }} + tls: + {{- range .Values.ingress.s3.api.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.s3.api.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} +--- +{{- if .Values.ingress.s3.web.enabled -}} +{{- $fullName := include "garage.fullname" . -}} +{{- $svcPort := .Values.service.s3.web.port -}} +{{- if and .Values.ingress.s3.web.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.s3.web.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.s3.web.annotations "kubernetes.io/ingress.class" .Values.ingress.s3.web.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }}-s3-web + labels: + {{- include "garage.labels" . | nindent 4 }} + {{- with .Values.ingress.s3.web.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.s3.web.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.s3.web.className }} + {{- end }} + {{- if .Values.ingress.s3.web.tls }} + tls: + {{- range .Values.ingress.s3.web.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.s3.web.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/script/helm/garage/templates/service.yaml b/script/helm/garage/templates/service.yaml new file mode 100644 index 00000000..2bfff99d --- /dev/null +++ b/script/helm/garage/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "garage.fullname" . }} + labels: + {{- include "garage.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.s3.api.port }} + targetPort: 3900 + protocol: TCP + name: s3-api + - port: {{ .Values.service.s3.web.port }} + targetPort: 3902 + protocol: TCP + name: s3-web + selector: + {{- include "garage.selectorLabels" . | nindent 4 }} diff --git a/script/helm/garage/templates/serviceaccount.yaml b/script/helm/garage/templates/serviceaccount.yaml new file mode 100644 index 00000000..a0a89a33 --- /dev/null +++ b/script/helm/garage/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "garage.serviceAccountName" . }} + labels: + {{- include "garage.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/script/helm/garage/templates/statefulset.yaml b/script/helm/garage/templates/statefulset.yaml new file mode 100644 index 00000000..82fe89a9 --- /dev/null +++ b/script/helm/garage/templates/statefulset.yaml @@ -0,0 +1,97 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "garage.fullname" . }} + labels: + {{- include "garage.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "garage.selectorLabels" . | nindent 6 }} + serviceName: {{ include "garage.fullname" . }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "garage.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "garage.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: 3900 + name: s3-api + - containerPort: 3902 + name: web-api + volumeMounts: + - name: meta + mountPath: /mnt/meta + - name: data + mountPath: /mnt/data + - name: etc + mountPath: /etc/garage.toml + subPath: garage.toml + # TODO + # livenessProbe: + # httpGet: + # path: / + # port: 3900 + # readinessProbe: + # httpGet: + # path: / + # port: 3900 + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumes: + - name: etc + configMap: + name: {{ include "garage.fullname" . }}-config + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.persistence.enabled }} + volumeClaimTemplates: + - metadata: + name: meta + spec: + accessModes: [ "ReadWriteOnce" ] + {{- if hasKey .Values.persistence.meta "storageClass" }} + storageClassName: {{ .Values.persistence.meta.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.meta.size | quote }} + - metadata: + name: data + spec: + accessModes: [ "ReadWriteOnce" ] + {{- if hasKey .Values.persistence.data "storageClass" }} + storageClassName: {{ .Values.persistence.data.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.data.size | quote }} + {{- end }} diff --git a/script/helm/garage/values.yaml b/script/helm/garage/values.yaml new file mode 100644 index 00000000..dd1c99f0 --- /dev/null +++ b/script/helm/garage/values.yaml @@ -0,0 +1,124 @@ +# Default values for garage. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# Garage configuration. These values go to garage.toml +garage: + metadataDir: "/mnt/meta" + dataDir: "/mnt/data" + replicationMode: "3" + rpcBindAddr: "[::]:3901" + rpcSecret: "1799bccfd7411eddcf9ebd316bc1f5287ad12a68094e1c6ac6abde7e6feae1ec" + bootstrapPeers: [] + # kubernetes_namespace: "default" + # kubernetes_service_name: "garage-daemon" + kubernetesSkipCrd: false + s3: + api: + region: "garage" + rootDomain: ".s3.garage.tld" + web: + rootDomain: ".web.garage.tld" + index: "index.html" + +# Data persistence +persistence: + enabled: true + meta: + # storageClass: "" + size: 100Mi + data: + # storageClass: "" + size: 100Mi + +# Number of StatefulSet replicas to start +replicaCount: 3 + +image: + repository: dxflrs/amd64_garage + pullPolicy: IfNotPresent + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + s3: + api: + port: 3900 + web: + port: 3902 + +ingress: + s3: + api: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + web: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} From 131cc2532b13acfb90d38e04c5dac5fa9cd3cb0e Mon Sep 17 00:00:00 2001 From: chemicstry Date: Mon, 20 Jun 2022 16:02:23 +0300 Subject: [PATCH 141/149] Cleanup values.yaml --- script/helm/garage/values.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/script/helm/garage/values.yaml b/script/helm/garage/values.yaml index dd1c99f0..06cf9d16 100644 --- a/script/helm/garage/values.yaml +++ b/script/helm/garage/values.yaml @@ -10,8 +10,6 @@ garage: rpcBindAddr: "[::]:3901" rpcSecret: "1799bccfd7411eddcf9ebd316bc1f5287ad12a68094e1c6ac6abde7e6feae1ec" bootstrapPeers: [] - # kubernetes_namespace: "default" - # kubernetes_service_name: "garage-daemon" kubernetesSkipCrd: false s3: api: From fa52558ca12573763bea392f9b44f71d3a6bb457 Mon Sep 17 00:00:00 2001 From: chemicstry Date: Mon, 20 Jun 2022 16:02:53 +0300 Subject: [PATCH 142/149] Add configuration instructions to README --- script/helm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/script/helm/README.md b/script/helm/README.md index 715cbab1..925d51ce 100644 --- a/script/helm/README.md +++ b/script/helm/README.md @@ -16,6 +16,12 @@ With custom values: helm install --create-namespace --namespace garage garage ./garage -f values.override.yaml ``` +After deploying, cluster layout must be configured manually as per garage's documentation. Use the following command to access garage CLI: + +```bash +kubectl exec --stdin --tty -n garage garage-0 -- ./garage status +``` + ## Overriding default values All possible configuration values can be found in [values.yaml](garage/values.yaml). From d0f08c254e3cc996dbf3d565df9ff2c89e15c639 Mon Sep 17 00:00:00 2001 From: chemicstry Date: Mon, 20 Jun 2022 16:08:41 +0300 Subject: [PATCH 143/149] Add secret to overrides --- script/helm/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/script/helm/README.md b/script/helm/README.md index 925d51ce..b7a3a35b 100644 --- a/script/helm/README.md +++ b/script/helm/README.md @@ -29,6 +29,10 @@ All possible configuration values can be found in [values.yaml](garage/values.ya This is an example `values.overrride.yaml` for deploying in a microk8s cluster with a https s3 api ingress route: ```yaml +garage: + # Make sure to generate a new secret for your deployment + rpcSecret: "1799bccfd7411eddcf9ebd316bc1f5287ad12a68094e1c6ac6abde7e6feae1ec" + # Start 4 instances (StatefulSets) of garage replicaCount: 4 From 37a73d7d3782ec8a5cd8b0e71a00722f90321ced Mon Sep 17 00:00:00 2001 From: chemicstry Date: Mon, 20 Jun 2022 17:11:09 +0300 Subject: [PATCH 144/149] Move documentation to book --- doc/book/cookbook/kubernetes.md | 87 +++++++++++++++++++++++++++++++++ script/helm/README.md | 72 +-------------------------- 2 files changed, 88 insertions(+), 71 deletions(-) create mode 100644 doc/book/cookbook/kubernetes.md diff --git a/doc/book/cookbook/kubernetes.md b/doc/book/cookbook/kubernetes.md new file mode 100644 index 00000000..8fd12fdf --- /dev/null +++ b/doc/book/cookbook/kubernetes.md @@ -0,0 +1,87 @@ ++++ +title = "Deploying on Kubernetes" +weight = 32 ++++ + +Garage can also be deployed on a kubernetes cluster via helm chart. + +## Deploying + +Firstly clone the repository: + +```bash +git clone https://git.deuxfleurs.fr/Deuxfleurs/garage +cd garage/scripts/helm +``` + +Deploy with default options: + +```bash +helm install --create-namespace --namespace garage garage ./garage +``` + +Or deploy with custom values: + +```bash +helm install --create-namespace --namespace garage garage ./garage -f values.override.yaml +``` + +After deploying, cluster layout must be configured manually as described in [Creating a cluster layout](@/documentation/quick-start/_index.md#creating-a-cluster-layout). Use the following command to access garage CLI: + +```bash +kubectl exec --stdin --tty -n garage garage-0 -- ./garage status +``` + +## Overriding default values + +All possible configuration values can be found with: + +```bash +helm show values ./garage +``` + +This is an example `values.overrride.yaml` for deploying in a microk8s cluster with a https s3 api ingress route: + +```yaml +garage: + # Make sure to generate a new secret for your deployment + rpcSecret: "1799bccfd7411eddcf9ebd316bc1f5287ad12a68094e1c6ac6abde7e6feae1ec" + +# Start 4 instances (StatefulSets) of garage +replicaCount: 4 + +# Override default storage class and size +persistence: + meta: + storageClass: "openebs-hostpath" + size: 100Mi + data: + storageClass: "openebs-hostpath" + size: 1Gi + +ingress: + s3: + api: + enabled: true + className: "public" + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" + nginx.ingress.kubernetes.io/proxy-body-size: 500m + hosts: + - host: s3-api.my-domain.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: garage-ingress-cert + hosts: + - s3-api.my-domain.com +``` + +## Removing + +```bash +helm delete --namespace garage garage +``` + +Note that this will leave behind custom CRD `garagenodes.deuxfleurs.fr`, which must be removed manually if desired. diff --git a/script/helm/README.md b/script/helm/README.md index b7a3a35b..5f919a23 100644 --- a/script/helm/README.md +++ b/script/helm/README.md @@ -1,73 +1,3 @@ # Garage helm3 chart -This chart deploys garage on a kubernetes cluster. - -## Deploying - -With default options: - -```bash -helm install --create-namespace --namespace garage garage ./garage -``` - -With custom values: - -```bash -helm install --create-namespace --namespace garage garage ./garage -f values.override.yaml -``` - -After deploying, cluster layout must be configured manually as per garage's documentation. Use the following command to access garage CLI: - -```bash -kubectl exec --stdin --tty -n garage garage-0 -- ./garage status -``` - -## Overriding default values - -All possible configuration values can be found in [values.yaml](garage/values.yaml). - -This is an example `values.overrride.yaml` for deploying in a microk8s cluster with a https s3 api ingress route: - -```yaml -garage: - # Make sure to generate a new secret for your deployment - rpcSecret: "1799bccfd7411eddcf9ebd316bc1f5287ad12a68094e1c6ac6abde7e6feae1ec" - -# Start 4 instances (StatefulSets) of garage -replicaCount: 4 - -# Override default storage class and size -persistence: - meta: - storageClass: "openebs-hostpath" - size: 100Mi - data: - storageClass: "openebs-hostpath" - size: 1Gi - -ingress: - s3: - api: - enabled: true - className: "public" - annotations: - cert-manager.io/cluster-issuer: "letsencrypt-prod" - nginx.ingress.kubernetes.io/proxy-body-size: 500m - hosts: - - host: s3-api.my-domain.com - paths: - - path: / - pathType: Prefix - tls: - - secretName: garage-ingress-cert - hosts: - - s3-api.my-domain.com -``` - -## Removing - -```bash -helm delete --namespace garage garage -``` - -Note that this will leave behind custom CRD `garagenodes.deuxfleurs.fr`, which must be removed manually if desired. +Documentation is located [here](/doc/book/cookbook/kubernetes.md). From b71fa2ddf45e21f40067fc021b3a81d738556eca Mon Sep 17 00:00:00 2001 From: chemicstry Date: Mon, 20 Jun 2022 18:49:38 +0300 Subject: [PATCH 145/149] Generate random RPC secret if not provided --- script/helm/garage/templates/_helpers.tpl | 26 +++++++++++++++++++ script/helm/garage/templates/configmap.yaml | 3 ++- script/helm/garage/templates/secret.yaml | 14 ++++++++++ script/helm/garage/templates/statefulset.yaml | 21 ++++++++++++++- script/helm/garage/values.yaml | 3 ++- 5 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 script/helm/garage/templates/secret.yaml diff --git a/script/helm/garage/templates/_helpers.tpl b/script/helm/garage/templates/_helpers.tpl index 1a651f47..037a5f1c 100644 --- a/script/helm/garage/templates/_helpers.tpl +++ b/script/helm/garage/templates/_helpers.tpl @@ -23,6 +23,13 @@ If release name contains chart name it will be used as a full name. {{- end }} {{- end }} +{{/* +Create the name of the rpc secret +*/}} +{{- define "garage.rpcSecretName" -}} +{{- printf "%s-rpc-secret" (include "garage.fullname" .) -}} +{{- end }} + {{/* Create chart name and version as used by the chart label. */}} @@ -60,3 +67,22 @@ Create the name of the service account to use {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} + +{{/* + Returns given number of random Hex characters. + In practice, it generates up to 100 randAlphaNum strings + that are filtered from non-hex characters and augmented + to the resulting string that is finally trimmed down. +*/}} +{{- define "jupyterhub.randHex" -}} + {{- $result := "" }} + {{- range $i := until 100 }} + {{- if lt (len $result) . }} + {{- $rand_list := randAlphaNum . | splitList "" -}} + {{- $reduced_list := without $rand_list "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z" "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S" "T" "U" "V" "W" "X" "Y" "Z" }} + {{- $rand_string := join "" $reduced_list }} + {{- $result = print $result $rand_string -}} + {{- end }} + {{- end }} + {{- $result | trunc . }} +{{- end }} diff --git a/script/helm/garage/templates/configmap.yaml b/script/helm/garage/templates/configmap.yaml index 587746f6..e33a4dbd 100644 --- a/script/helm/garage/templates/configmap.yaml +++ b/script/helm/garage/templates/configmap.yaml @@ -10,7 +10,8 @@ data: replication_mode = "{{ .Values.garage.replicationMode }}" rpc_bind_addr = "{{ .Values.garage.rpcBindAddr }}" - rpc_secret = "{{ .Values.garage.rpcSecret }}" + # rpc_secret will be populated by the init container from a k8s secret object + rpc_secret = "__RPC_SECRET_REPLACE__" bootstrap_peers = {{ .Values.garage.bootstrapPeers }} diff --git a/script/helm/garage/templates/secret.yaml b/script/helm/garage/templates/secret.yaml new file mode 100644 index 00000000..54749424 --- /dev/null +++ b/script/helm/garage/templates/secret.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "garage.rpcSecretName" . }} + labels: + {{- include "garage.labels" . | nindent 4 }} +type: Opaque +data: + {{/* retrieve the secret data using lookup function and when not exists, return an empty dictionary / map as result */}} + {{- $prevSecret := (lookup "v1" "Secret" .Release.Namespace (include "garage.rpcSecretName" .)) | default dict }} + {{- $prevSecretData := $prevSecret.data | default dict }} + {{- $prevRpcSecret := $prevSecretData.rpcSecret | default "" | b64dec }} + {{/* Priority is: 1. from values, 2. previous value, 3. generate random */}} + rpcSecret: {{ .Values.garage.rpcSecret | default $prevRpcSecret | default (include "jupyterhub.randHex" 64) | b64enc | quote }} diff --git a/script/helm/garage/templates/statefulset.yaml b/script/helm/garage/templates/statefulset.yaml index 82fe89a9..bda40117 100644 --- a/script/helm/garage/templates/statefulset.yaml +++ b/script/helm/garage/templates/statefulset.yaml @@ -26,6 +26,23 @@ spec: serviceAccountName: {{ include "garage.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} + initContainers: + # Copies garage.toml from configmap to temporary etc volume and replaces RPC secret placeholder + - name: {{ .Chart.Name }}-init + image: busybox:1.28 + command: ["sh", "-c", "sed \"s/__RPC_SECRET_REPLACE__/$RPC_SECRET/\" /mnt/garage.toml > /mnt/etc/garage.toml"] + env: + - name: RPC_SECRET + valueFrom: + secretKeyRef: + name: {{ include "garage.rpcSecretName" . }} + key: rpcSecret + volumeMounts: + - name: configmap + mountPath: /mnt/garage.toml + subPath: garage.toml + - name: etc + mountPath: /mnt/etc containers: - name: {{ .Chart.Name }} securityContext: @@ -57,9 +74,11 @@ spec: resources: {{- toYaml .Values.resources | nindent 12 }} volumes: - - name: etc + - name: configmap configMap: name: {{ include "garage.fullname" . }}-config + - name: etc + emptyDir: {} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/script/helm/garage/values.yaml b/script/helm/garage/values.yaml index 06cf9d16..d011f63e 100644 --- a/script/helm/garage/values.yaml +++ b/script/helm/garage/values.yaml @@ -8,7 +8,8 @@ garage: dataDir: "/mnt/data" replicationMode: "3" rpcBindAddr: "[::]:3901" - rpcSecret: "1799bccfd7411eddcf9ebd316bc1f5287ad12a68094e1c6ac6abde7e6feae1ec" + # If not given, a random secret will be generated + rpcSecret: "" bootstrapPeers: [] kubernetesSkipCrd: false s3: From 744c3b4d9487045ab04a221572722afa0ca34b09 Mon Sep 17 00:00:00 2001 From: chemicstry Date: Mon, 20 Jun 2022 18:52:32 +0300 Subject: [PATCH 146/149] Update docs --- doc/book/cookbook/kubernetes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/book/cookbook/kubernetes.md b/doc/book/cookbook/kubernetes.md index 8fd12fdf..0bf89c96 100644 --- a/doc/book/cookbook/kubernetes.md +++ b/doc/book/cookbook/kubernetes.md @@ -44,8 +44,8 @@ This is an example `values.overrride.yaml` for deploying in a microk8s cluster w ```yaml garage: - # Make sure to generate a new secret for your deployment - rpcSecret: "1799bccfd7411eddcf9ebd316bc1f5287ad12a68094e1c6ac6abde7e6feae1ec" + # Use only 2 replicas per object + replicationMode: "3" # Start 4 instances (StatefulSets) of garage replicaCount: 4 From d2c937a931b6549ffd2f2afdd7a871be8d8eefbf Mon Sep 17 00:00:00 2001 From: chemicstry Date: Tue, 21 Jun 2022 16:16:42 +0300 Subject: [PATCH 147/149] Fix typo --- doc/book/cookbook/kubernetes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/book/cookbook/kubernetes.md b/doc/book/cookbook/kubernetes.md index 0bf89c96..9eafe3e1 100644 --- a/doc/book/cookbook/kubernetes.md +++ b/doc/book/cookbook/kubernetes.md @@ -45,7 +45,7 @@ This is an example `values.overrride.yaml` for deploying in a microk8s cluster w ```yaml garage: # Use only 2 replicas per object - replicationMode: "3" + replicationMode: "2" # Start 4 instances (StatefulSets) of garage replicaCount: 4 From 6dba7dadf44781abfb878f06fba86e731b267c87 Mon Sep 17 00:00:00 2001 From: Maximilien R Date: Wed, 22 Jun 2022 10:04:59 +0200 Subject: [PATCH 148/149] Add missing ClusterRole and bindings for CRDs --- script/helm/garage/templates/clusterrole.yaml | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 script/helm/garage/templates/clusterrole.yaml diff --git a/script/helm/garage/templates/clusterrole.yaml b/script/helm/garage/templates/clusterrole.yaml new file mode 100644 index 00000000..fa3e6405 --- /dev/null +++ b/script/helm/garage/templates/clusterrole.yaml @@ -0,0 +1,28 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: manage-crds-{{ .Release.Namespace }}-{{ .Release.Name }} + labels: + {{- include "garage.labels" . | nindent 4 }} +rules: +- apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "create", "patch"] +- apiGroups: ["deuxfleurs.fr"] + resources: ["garagenodes"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: allow-crds-for-{{ .Release.Namespace }}-{{ .Release.Name }} + labels: + {{- include "garage.labels" . | nindent 4 }} +subjects: +- kind: ServiceAccount + name: {{ include "garage.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +roleRef: + kind: ClusterRole + name: manage-crds-{{ .Release.Namespace }}-{{ .Release.Name }} + apiGroup: rbac.authorization.k8s.io \ No newline at end of file From db0c8b3980c5cb056c9402332dd09a1bfb276997 Mon Sep 17 00:00:00 2001 From: Maximilien R Date: Thu, 11 Aug 2022 01:35:41 +0200 Subject: [PATCH 149/149] Updates values.yml with some opinionated and untested defaults --- script/helm/garage/Chart.yaml | 2 +- script/helm/garage/values.yaml | 87 +++++++++++++++++++++------------- 2 files changed, 54 insertions(+), 35 deletions(-) diff --git a/script/helm/garage/Chart.yaml b/script/helm/garage/Chart.yaml index 9455488a..56598ea4 100644 --- a/script/helm/garage/Chart.yaml +++ b/script/helm/garage/Chart.yaml @@ -21,4 +21,4 @@ version: 0.1.0 # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v0.7.2" +appVersion: "v0.7.2.1" diff --git a/script/helm/garage/values.yaml b/script/helm/garage/values.yaml index d011f63e..08d0c09b 100644 --- a/script/helm/garage/values.yaml +++ b/script/helm/garage/values.yaml @@ -6,10 +6,13 @@ garage: metadataDir: "/mnt/meta" dataDir: "/mnt/data" + # Default to 3 replicas, see the replication_mode section at + # https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/ replicationMode: "3" rpcBindAddr: "[::]:3901" - # If not given, a random secret will be generated + # If not given, a random secret will be generated and stored in a Secret object rpcSecret: "" + # This is not required if you use the integrated kubernetes discovery bootstrapPeers: [] kubernetesSkipCrd: false s3: @@ -24,17 +27,19 @@ garage: persistence: enabled: true meta: - # storageClass: "" + # storageClass: "fast-storage-class" size: 100Mi data: - # storageClass: "" + # storageClass: "slow-storage-class" size: 100Mi -# Number of StatefulSet replicas to start +# Number of StatefulSet replicas/garage nodes to start replicaCount: 3 image: repository: dxflrs/amd64_garage + # please prefer using the chart version and not this tag + tag: "" pullPolicy: IfNotPresent imagePullSecrets: [] @@ -55,66 +60,80 @@ podAnnotations: {} podSecurityContext: {} # fsGroup: 2000 -securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 +securityContext: + # The default security context is heavily restricted + # feel free to tune it to your requirements + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 service: + # You can rely on any service to expose your cluster + # - ClusterIP (+ Ingress) + # - NodePort (+ Ingress) + # - LoadBalancer type: ClusterIP s3: api: port: 3900 web: port: 3902 - + # NOTE: the admin API is excluded for now as it is not consistent across nodes ingress: s3: api: - enabled: false - className: "" - annotations: {} - # kubernetes.io/ingress.class: nginx + enabled: true + # Rely either on the className or the annotation below but not both + # replace "nginx" by an Ingress controller + # you can find examples here https://kubernetes.io/docs/concepts/services-networking/ingress-controllers + className: "nginx" + annotations: + # kubernetes.io/ingress.class: "nginx" # kubernetes.io/tls-acme: "true" hosts: - - host: chart-example.local + - host: "s3.garage.tld" # garage S3 API endpoint paths: - path: / - pathType: ImplementationSpecific + pathType: Prefix + - host: "*.s3.garage.tld" # garage S3 API endpoint, DNS style bucket access + paths: + - path: / + pathType: Prefix tls: [] - # - secretName: chart-example-tls + # - secretName: my-garage-cluster-tls # hosts: - # - chart-example.local + # - kubernetes.docker.internal web: - enabled: false - className: "" + enabled: true + className: "nginx" annotations: {} # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" hosts: - - host: chart-example.local - paths: - - path: / - pathType: ImplementationSpecific + - host: "*.web.garage.tld" # wildcard website access with bucket name prefix + paths: + - path: / + pathType: Prefix + - host: "mywebpage.example.com" # specific bucket access with FQDN bucket + paths: + - path: / + pathType: Prefix tls: [] - # - secretName: chart-example-tls + # - secretName: my-garage-cluster-tls # hosts: - # - chart-example.local + # - kubernetes.docker.internal resources: {} - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # The following are indicative for a small-size deployement, for anything serious double them. # limits: # cpu: 100m - # memory: 128Mi + # memory: 1024Mi # requests: # cpu: 100m - # memory: 128Mi + # memory: 512Mi nodeSelector: {}
  • ;Ps!=FV)=vL-^F&ra( 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�bLJ4TU?0b&X41U0q&E3g)|$ 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 %?b}%cD(=5`wpU;KKcIMcRfx~ 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:Y~ zG5R-yKotdk((Qx@l8YJ9!CNfw?l_?hKRdJwNVydlPD76$_i)_MVMiEpkSbsd zAbSwttw&nv^>|<36F>$*S8CWaV%Vx`34(tu>d{A814P$T8z$hySO;3f4Rc3 zV@%=U;pev`prOQMigS*9KVS-gR8WKvt#$>U9N|TRdetUTPZPLZAUngY!+l@7YgH@m zu*Az2evr`khe-6(1WRziB$*Y(%m8V``f2wt2gD-Pfi%yg;>H5#AapB!l@5pM8kZ0t z-b!d$z^f)g`70D%KPNhBYhxp0ALyr+)Q2M#t*~!j^f^2FI*lT-lvpwro7JCEO)D+BD&4d8ZNJ@1sY@ zFeL%!g(#$X@OxTjDOvbPzLS?=X!EJ-AWP1(BSCLvIa)F*I3H(8!XOo1A~Xu18jg!!{92J!W0xmKy8&MD_jcTjA$e48MS%= z2z-0hVb|ZYI@*KKwDqs=`^*>jRQEY#0fuWYue$$KgaMQlK=0d|ingnW6pj@Q`;G2^ z&;y$8%`@}CYDr{2Yl(3LT4~(n=;-L%q1e~B9T*QDP2700Ydo_D7XUN_8W7Z!9WMt7 zhp5wt{tiA3`~+!fuQTzv&z@O+%y>^H5b!Gj=W)01A8&M~>Ow!% z#OHf%#6RtU<8qH@2ioBYm^O1z%K;=EttNBVfVS^ql&CzHxpAXcQAP&Y-9jJCa;?h_ z%7SL_KNzBJTwTCf@*U%^O1eJDq=Kp-$oU^zKoU5|P^DNlY-kzyxI3b@c~{+lYw-oT zpZr7wqpScdhYor4n>Q1ci0@)^0>$48V_VRrrmK60`gsD%&RImXf+GE|g@4(Y5Eq9g z0qNc|iJEcni69u|Af7*Bm=IvE1R;PNi}#v)N`$2EvJeA1gf9RkO?P3UF6z@-2E#Gp z>XkKLg68SXo8_F;K{TcjTF^oCwW>Pq8^9sFHObyR|YZ;1*zB` z^~$DQ2`ap!RpVS`3u{pmAcrt?6&8?^&Em(?iac4YY5ieg5}av-z6e*2Y54ckCMye! zBUsqNMo0Z?XebfFGjtxNYfgZO1v=D2m}~HWs7C10mDBa-+cm0(XKB*rJ#Q==P4smK zi54sXxCmoO@kp%>$6CA;i6T>!J1~ADW+V&Zm;dGun9I%T?riIHZa(fUCc*mW^T&^G zF+d>^^gEQY!U&^mi!Xk@h}G&7Fp3&ZyaKz}VOJb3Jyz%s+wcvQ5rU6_a8K(KUyonEet<>~w6c=njsGB*2@p=`gX#gZoCS(W3r<~t_?1G&{v zP;epf%%!BIMO~w#Bxewa9|+C0oSde!3o|1IZ#JL)(b=r{sk9RhvH z&+<#}3Ft2bifomZmOi85ogKh;O+W1dm<-%3s4T>;b1DVbOAWrSt2+;KFgbwHuHD8e z_3MPXD>2qKFfb^Si>YOR1{B`@w%m*BGfU<+K_0~u-R%LqlUi$glbA5H2fz(ThXKLy zkpsq<^x@;Y2JPiJS6+b+-)21zVQCinPy`+TNf1_KH8;kk!y$&dLe5u8))-aDbhw{T zJ;Fl;odA-fRCUCLC%^w6uHHNv%eMU_g~2n;lV!!NH(m zWrbJ)2OXgwOLP|O5iv^_14tN~u?(Siz!r?%LPBc(v#$}$@#@tDTn0wE2G zjy{E7>aj36(e8z4xrk-W#E;E8pS^qgbBw1YKw1Ng-s(AXzbsLKfTX9Z-r8?>taakx z&+`{@sA>F|s)3tXO2wgL!nK`u;|Q-gftDaQU3I9g3CDiirG!HN-VuCLj3^B~@%b99 zvasaIBGDuUvJ+GQVM&U8S|UWs1G-GC*SdV=$|I6vasO2u0ZcB1LRyYd=@32-Q5fx9 zYx-PvskXLu7kUF!&di}_fg`?D)241JB#`1cchxgzc-lt#6fcPWQzh*DTQlLtqw?;| z1yN|wc2S^@M)LAl`|^*9_wM26Q~8j{CgU!&25Ub2Lc8VCdI(_MaO;cEumDmUN{u$@ ze{hQ196&O0l8KdT#Ozx@RVCUpVsvwQ&3Pd7FhUlwvSDL=YU3;3gDMi|^b85LuNyAo z5(HF`r%*r;@&gMU!7_-wrH8S1KYn;Jyf>7!iL6fO31CbaC2#QJVHr>QJ zCTUJs7(0Kd5kN@#a;V`YkRXg(zz4PGseH}xAlD}&1nUAZG$HT58}`BZpl{|@O|STk z@MgtV+gU7w9G2$$KinCj zE`MMY;+Sx_E}_*$`ASMk>^4k{4ndxQh0F9TI_H0`ATM0E~Y^DLH>OO#_40@P?Osx{{Wf>iG0GMRyQq`Q>JvE?N`T zQfAutaz{?*swzr(z0*SHWpo7xH20EG5du`$Z2v|Wk4$e=yr6_$59K_HwEr4~hfiMgrXVfy=7&1{G&&>Zj@-w(eh|S^!EP=f5uOt z7`Q#W=0?v!2Vi1uZcCe^XFIP*A$Dj!KkL8mWEq|zqCnT!CHzj9mu!C6kDE&HUsbAE zkhS5j4xpAGE`$`ed=Ix4Ovx zfbHgFepQZxm^1x^E|1N{q3BTl4=+9yFlhb(8A_|H9b`2U&Gf7ZI)yvf$!3w)*xbBJ zokRK4=TD!AHsJT~-+i>g;Hy`TgVm2Bp@8HWfOrYB`@#52Oqgx;CIKfUo&wrFsH|qpa=(BcxxpiqpK6IdqcND^ScK*3heTYKfhFg+q-sz$K1b95`Rh)M8%PmC3-yPSmemr zmfPPFHtrwR$*hv%BkKyJ$oZ-<7|?9nv(isdAmFEc)2+({^ybNvC$B9 z-Te=|tU=d>Noo{{aQN^bj6i-~)6&wqrYRHDbmix}ThPwufxN~TfB}}Er}Dh8mB7&~ zHMv?5?CN=8OHHTV4+%>a!=0ZuxtG6}KBn$aYhH88^iO*O$KuJL5Y~az8b!!H9c^mf zVYuSm#RBs@1BiBT@0p(q(!%)>6HFoaep^IKoJ+cO_4RcCZkpsWnv{}Xwy~i}NW`z zP>!j_1{q_6z{8N2KH)VsH2e&N7ApkoU}JR=6*W@Z)D!-{mnHxI!ZUUECkaz;*5~Il zZ2UT-NWJS99TL^uRx+=Bw4!UG^^li;m-VS~&$(6Pa})x<>iOR9DPP#L#HaWHl4Et? z2LOyV9nCFn92GW<*%K6yCCRkwJ>B1cisz-Po_b!mzQ8=wMOgfOL|Zls?%VgX{&4Rv z=p#-(Z)y7e{Sy?i?YT8Y{UO4{+z!y(RZupBOq-ZkLFqsdeU-?}(5kNw`K$*+IDD&j z_4na_QziiZEnWJm4J-<~h_?45wzj-Q72r0K8jZ9p3nCg=qm#z_r%#{y5`qnoOWv3r zVGzvg*?nZmDYXZNP3SoV4v%NkUu(JdIXZHB?0}ao`_AoKdAN&T37tmK1P()YnZ}VO zF%eM}9eUAa!0cz@D4(~e+S_MSCq7xyK)QK_Vee}Y;5*Hmj}J1h|E<7 zhp3aE(02fJBqpb@I-YO{Ko=5F{gYU{2>iFaH+;4was3fNBtq}HJ}bZR5als!jjHgh z=#IO%ND-qRgis1;{}+6F3H&f3rI&3h4-ed%eh}-}7MsGo-@{+22&-zK8F0@1dEl8I z)H1{v2Ef4qV*T~d?A%IuNHSVBq>Z&=UVTTpw3NN{uBubFz9d{6cr^9blJOhlIR1O@ zLTZjm34>--!R<86wC>B3t?ljV;!5AOH;WGDXEIrQ0xC^OMFJ$j=7q`)>Wbe`%~-_MBw1uW6iv2XajV_pHusOg z*+g&chRHS4yuJ*-ZZKl@MEr$G$7fA3C3(RRMT9V1LzF~@^MZ;WVX>tO>p*DDb-aY$ zh2bSJTV&}A6U(G7OQ?wx!Z!4Lgk&8V&Ii41u<6?9xgRcF_C#u3JctsUXs|F2_89=Yn88eRWaKU2NiYIP z4lgT|s%(wfN|_pMYRYcZ%{euFzq4V?F(jFAEF}DNh%Lt0^q|d(1`VA6Ofyg^xL1vY zuMuvOU3drDi%^f1mTCAHxPoB7W zvyehPkkhb7Sn2jyL%smmo1YCsvok($b;R4kOv_2A-~gsy^&H@MmceLcWW<3|kJN|e zeezHdkPnE2exkP|HfinfEIljdS|CzcSm8Ezf6tl=kMx&w!DElDXHEaUfoUJ0IKY@o zs6luLaW4U^yO)8bgCGz?DnKfTa^BK%z}h|z)1<(Hy1P$9Z_mG>WKi4Ca0>YxxI)Z# zVZ|so;6X4FuX(d)Y6gWan8VcbdVzGr&7}nP20WjIILN!kqJ4L5CGGYGuJi3}8puAOnY(p(A4O3OA?Tj7s`aCv2yBXyx zE}K@^l0&Q+^X(+ z5PVl^Doo$L%22<@h?LXOk;%!ARtH04Gqeu8g7N{{Q}FQkdoln+Z1WR@%_oMHAdx|s z6?mu`=AmpbS|q-AC6{_z{$APx{^Wkgr!#CTdS1c_6Sf_<52$3e9>jWbwDL86o2cN> zdl42#pdPXRg$4U~Zd4_)C9M`CH$bSvK`fOM#4=1_VY}md)j#GM2}>3oeaXKw$V%pn zlM}O7!ZhY%a6bZQ+C)`*;;&|3-3pxOGo-Fs7 z#I+T77N6~RH$VK$szFZcD2K#GLh!1cq&Rr`4{Rt zx*ZM^gX1X0p-q71mHvrlNa3qj6vBqLsGTuqR?Mh`6-Zn7BE1#q3ILFR9reLdNL56X zHT%qtrY1i42{9>(JCEuTUla2`iij74WccmGn=aRx4?Aq^?deO}F+)SpbwoBdEcZMD zm}Zkn0{gb~tm5uu>5sVy zFMZYcvoAyuaSX3_=*L$uV4a<6C|U#_?)z@4cDwvZ4C}mm8UA$?WuLx$;g^(*#Eil} z+DLe@0aPMlJ=gRN1TJ8-K0PVrsMVlQBzmrI-vVm}u%*(Xpo0Jj(5(KU+RC>dM zx*MT>m!XV9;X2a3hO&*{Rgs#{J^sQEpN+L20O5%RbHEg)|I~@y zsHQ&*iIxsd+`Hee@yWdZJWRxGugA0^h#zRpUiK~=9@Uj7IvR9kCH8#x<1a&CAq$}t zAq2`MkL~R3Q(wQ1M146cXL80u44dVA(I`U+izEe}Zw5YgPdplPn*OTwDJv-v>SSHr zZ%6cej&(I3>S!Nv>R(tHJiYsCQ-LXxE-tJ?A&0UygmhS232;y}dhE$s&lRadpbG(! zxm4BgfH?qwf*O4Dxk4=SUGi_q#6dyNU$s$aAVTc+gPy2nV5ERu|F%}+p+_0zw_(#NW*WG>zvm4DN1*ped^y3@{7yx7Y${V?I0gA4=I3iu3? zw~2XWC<5*S#Miv(K9PsN)XT=EkZ_E3q5r`iyiL!kc0yD@Xv(V0hFcrNckS{AS8vwb z08sm(=YP_Dh#-j>osi_e=SQhHdEr;Adlt5ty&r_&$=7f{<@uNGg2<+dV z-H?)qzr@_21be`p@`MF!mB0dAtn?vvUqc06I(+bFf*%bbZU)hhfXJ6wA^bLwO>-3d z;~!O;0H*2%Wn$+H4DGV@w=y$3Hl=%E)#f>v2nI4Giy`g;kHCyN7MCL*=?fV;1ixEb`77b_#ASVQ!=B6e;JWWy2 zoO8PnMwkK0!Yl?5@;hjmT}m8Fx&f!+lHs2L+%}V7hnTQx-3I{-ba1j@m35?C;u%fN z&DV-EaxPgo_)xhz-Mk4j69WMt#X&s~O%3`W-~nLAfj;4Ag2I6ajbI6rp;>|~8J#@| z8{eN56g)%(To`lP3#3d*DG7Ig$;1{rPh*0;W6mFc7UuH(Z_5)&_|EVkQ_FU50wc4BH z5_^G*pZyt?wT#?aGIvF9O$vXjTg7uYq~>y46K}L)Io=I3V4GWF0My_aK)i*%9H|OH z)GT6BM?euhe8m0xm3VsocTP#im(HxL0TC}QA)etu5p%ku&ZFOy#S{~>(P^UxN9W$K zgkjYi*A|1AELA-`WRRgAe&!##w#$m3HJJ9$V8VeZy3R~|ns9CaTGZtUOqIA_L-98c zT7IO%ZNe}*9{}0HID%CH*+JMP{YMMDADc_Kxz8+wxw${-SNFm4o9c2821KXC3;VTp zZ^(Nr@N`Le_b{%Wz7ush=FR21Q8q?loWe9-+AKwmqVki?{3S-gM!D_Db|u-}l&_3} zlTI`_8#S2z0Fiyf?0WK0gz_7;L59HcK92Vx<}-pQ7T;Etfb{B;mvnJ}3Ud$kO?5z|aWZ z{lM15MGCxfGd42LKtpF}%`ieqimO2jdeA|M_y>Y~9_h5Z2~Y zs7dKQon)aP#M}9bs3F3`{K#v#w9QX6x}04ZO+y@V@4A=I|OG_Ak;t>#`E33pjRN z3Ff61W*58?xoz6c&aOaa{cC-FN1X_d6&oeK5)(WaP>A-qadXym%pKuksL6S}?hQeRb}Xdn z+>Pa2P~3g73Kcb2rz1dLx>%?1MBb4UKHlpZT5(HxQ?Bu8UiOlJg~aaQns?a%Yy$XI zFWcLHfav5L2KZWU^;=$#c(&19QIKv17;PL$@kx>p!~m@@TRA zqBLL0or7!C>Ab}`mS$$Yqa&zWI}X5;aq)wgw;!=mW?*1I+GAcs?$3kp<57dgD;oOS zwWQ*d0V1<1;}p8~-`RT&?zNtS!lqT=T8pO+u&D46=zO)|9o$o{`7UFrGn|?tHW64QWmcj2uVz z<(d#|6Z<$J;UgBng$!+{-HiEVQ16nxQ}EpDlV-pM>D=;VA+IDgUu#3cS)3ubIw$=3 z*m6Qfkzn0He${tqh{}`pl(fxS=CQTX@|V1uTF5vi{wj z;@^0SjxX?j_h0m%7-KZ4gmjefX!V4Ony@i?^G()5=AS7)5Wjd$OGiiM2!8V`SLg_Q z1BBu|3{!6>VOri7N)&)C=o&hiSu)7Ho`s9)ljzfnI!5{m4Q-gHaiK|z|h7mkc!ZTJ;VwX7^>~jNzH!t+8R`F0bMlY9+ds3I zP|rq7L5}4T9HKTiFoFy5pMx|kA&TZZtPb9BI!d40Zg=?7FKLDt4N4R*&wP2(rC*gs z-=|F)JGyxlo#uE3V2S~V=-+y+ND;Q(p0jbi?{AjRa+OYbO;vmJu9&!%_2m0dCJgbZ zvXJ_K>%4WF6)=d}T$hh`{t-zoWM;+@`vJG*0y0xh(t3F)87@!^cPZx`H*46@rCq#4 zM%w*MX!HqPpnGNer#>z&^^LNGJ9lWH?Rr*J)ZyTYtI!0oBOdh11@7<~VQ-nC>qlg6 zN7oGk(GG#O5bip9xylp@sG9te1?R7uR`gyyypI0jw$d&clhX2Rdz+Cr)jtJo)$*C; z5*vCPDw}o6)Mx}=zj{TgOeh!CW!;>lMU7Jm2WdvG6(~R1f)vQ)^T+6_4Q5YL^F`h! z?hezp(f}q!mDDB)dikZ#XQoXz=1d>Q$A5sS@*Vk=0{LpBz7X4B!-r2N+PeDPxM7JY zQkUaqa>1nVN|{6XAg?Nml_4&Hi&T=NCVEx99VrmCp|T>c%SSAL6|uiftjhD2PZta9 zCbt&n3;=vU9@w;L6Uldd-TO;@{m;0Va^5mTW*oVPsl~W$no{s3a$`vk?QJdkL5)R>xSR6ZgdO`IZMXB-MH7* z)+7%Iq+B!H{fua3GuGHA$c^@Hi^W;xUq1H^F9`hw5K4j%xVVHkk; z2`o^eXQgMMvs;PmyP>JkXW#ECl?_KtC)GMyVz zr?&=~`8EnJ)qzQ&K!?m$#;dF4u$RpJ^h7iv*g zK*8frxoRAh6L-hecan7mTXTr^GG6`hHt%Ba@WklY*rtYAfbpM(Wr$q6%9*;KY{p{P{#X4Am_h9AH1V1u4nGf*eb&D*yhKfTSl_Ltv9OYZHd-Au)-t zu0_S{Azwg#KQ;~iUksyHmH$^UVLFIPHG&8su%^Na>3iYfEjyydW1&+81Bbt$m3x?> z7~>tNOIaz?() zffQhE_$i-GoAp7%5auN5ihP+O{+PYgASs`xvZQu_)kH2`wh9R zupeO$)p>erTqw8wZh5)faB&zhw?rAJOHaGZtfL@R!Wly3{e5+IlY_Hv7fM|nKin{Z zvtB7S5fJW@?j9a9xZex^O1GvaZ5hGpd=VvWB|%n%D1!?Z-eZSO(V$syn$04_iDl1a z;vF9Gt1iMS!-h)zP4)Ss&~7}?5Ky0AvNAl0QwR_-Z-R90%o#>P{Vgj?r>y;E&V;ju z&|%KAw4ktoyxghB?*W<+0c-M#)q z^flz3HZm~40smIarFj)TpHSA25d%(Dj-2buxf|?>i|_Yhk*Bdoy+pm_mW8VypUbb= zATso$fRnRb%8xQu^xbH!_ zVYP1nhV#TGCt!{#zYAM2xM{ht|Xz?W!*vX)8tVYMzIAY^?A`aJ2{!TxI{(GMEeB< z`QC3Y`PW>EgQ`;?mN$ ziJAZsvPDo?`uQ9eQVqTq@4(>qqrYoT#x>K^;+zb@Wjv2yWsl2;nAInp)BjPs-C09Z zliauafqMY}a@4Q~WKjo7<8z@&1i^Ou)~(GyO5Gm1=qqOE;>_UFMzHLGvn6lHE+uV> z8pmtWg1UM_BJ^Z@snII@NR85AUCl}mk*h=~?cIuWRWb^M8@~^p_iP*-U4K>p`4SQU zqIiY(<23Np0YWD*gNBj{=1?osJqzVKFl9hlN}k$cgO8^e!LJgbo4@R>UwKo;uNS$L zBxrcJZ!k+oxNXYdrs9YE;qh@Lh*)ry;c3AZw9h>kRg|9IzPq>X3&G+CpKAZa{-nXf zk6aT>5BPi?ysm{YgBv#_42*V~vXd*Sor$t$3nJ6gD}3Tb?A3gcaemhL2T^D-Am%kT zlJxY>)lUz&d8?Dr8Ai>dih0ratQ7j(g_epNpM2vE!C%x6B?obChDSz{s&d6=Tsb57 ztH$P{_=juqMJN=GCux1xZ)~7tjXre$x6{#cp7V%l)c(FGY#@@&{iKrT?}36ZBSYu& zcE$PID*TAz_ca7YJd72AnI*|DtgI2`Uh7&{?U88Wh-{@u;@=VT^BNn95s9bh23?*s z3~eIG^wl_WLRIQ1msrN%B5@jTZ!0D&;=DJaqPDNr_pZ{*EjNYt6Y@>2g!*!GA zOJBjqi!LtPGcr0ABL+!;PW~kC+NJ|10fhWW&wGRrYSj ziIDMmu-G*yg>ZVSYeKZvwb_}gIOhZt1!~Ky!ma}@B{R^D^u=6CsH&_a5#{Y_i~QPF zkn_`ijGiQ{*<=L_UGET#qeR=4M#cQ!+ zZ$%ccS`jh+acr#n<+V@Or0IgpYm48Da)+OYvR)}Mv!1Yt4Z0#_)aJXJFC`xpLB`-Y zN_~2HosVNja7u)X07IjA6TJ0=N%2#d8b1qKfH#{Q^o-otGgQpbGK|ev&A;{Y4RHKAvZY2ZQ0h4W2jz2OwH1vXg<598TvH1Cnw?8JTS1UFaHm33EcR&1`^+NKT z?aIOwRb4)weMs^nr;&_gLD=Hnx88|L-b5`yEtOZbx%lu=Yr~UXUdMFxQWcj2?{mdJ ze9+vXRw));*Hmf$FA$SQ@bf)TX=Fhh=9AHEcLq!cysd?SJUxR}o7Ft_qG9w=931(j zhw^U2vl=ZwZVV*U4NhYmZ2%x{P`ASTZk{FTawHndix(*YJXmuDgEsgz>!$RqDL;#N z&M2^D8>+|VcExM%W~1wrHwq%{#R6*r&M7V3Df>PRiv_;{>D8`^H4Xyn zLcLG8siGQkiLx&GNc4+z)n@MQ#aUhL;&d-EY^fe7YrltQK}Newn>WDFy6w051Mk1a z4tZ3oyO8QwX}59Jn0PN)vp=PW6ah7PhfWZWq-y6;qGs5GNWE|P6kh5nI0U0UL}5or z7+uofdq?%MY%=rLPojc=G?j$Gph{v&7)=2yF-CJi^qiqj?&~K<<29$g1O{3&)7~C(;!9@&Dy%xGE|8Z7(ME?yNFn`fSBQOu9K@?W5;}ooO}ZU2E4p zvVZTF(bIoF+9Q4F&{)_{0ZurdEG6kpTnXS{4{yiIwT zUPjXdEAkPCV?(pBi=KqeM`W23j1LkNaspbhS3E3=LjHmw$Qke`4^tm|nZF{HsU>(E z(_Z!UO}tSSGL1}3I>#^(beRb$;*l}%0c4&28EMG}83!WxJ2v)HlPhTlCJS1x#RJTl z_ufH!3-v94QIgq}EB9*Sm=uMsYrc^^6q5ze3_y-x2#0q)ti3RJmBzJ433oSxy2eq0 zngIBOfJeX@4c}Ea^97}wEH@6|6{#&@15C8Ho@T(J&QK@)k#T!N#=;}DRIJz%9M+&t zI${_JQ!|`$z!=n_R$&wC>?P#_DAmyye~9(O&a|&z6kJ;r#9?P#=$vb!oTxT-XodL z-%xSWY7DX0Z|$Gb-CPY?(DryF-{TGMg- z%6&WW!09N>V+C@s-%2lW&CYa%y?kxr>c9z}Wv%+FH87Lue}--}%Cte84%ss!W8N|( z`M93m?PO{NrVwx76&{hjQ>S>Dj(d5Ptr?&}#2^>aJMQ1*ZEaU`dC+{<47goRJ7T>s z!SGSy^ur*(`=iXECrA8oUQ|?XRh!>Vz75a@)o8QtH(muM*sPO8tQYoQ?SJRUi5acU zr(49u#g7LlQ+KpL@*al%X{DF3?ODOWWZd2U8Q1zy#=3kg6hGl7r!E?OLMSphr?e!m z>@qcVx+uyme*#ceJIO?Wk6bM-*mrKAeMb~k?ja}RQ$msc{Hl*8PEmIhfG#Bl<59!C z$Ov(jHF@<;jTP2>a`=zXSUMdwzF}$Q{JUFY?K>J(@PA|##>9EZxW}(*U|?_@zqAUI zRrzIG6ryg4)QO)a6tNP#k{V1DBz3a#*4o=zmy{JhnS6BQeD_nFEvV}I_wN~8b=#By zA?dY_XFNg?%QgMuHr}hA-T@gIb(!xDCSy&wkY0QF4WobsfD`h=PMP>gobM~D;%q2U zaj|1G(u6iBQRG8H$cYFiX*?S>Y-w2Rq(Lre7CS_kPig(PdaxNc5*%EQ;XzUQJLMSueMMRWp?nqqYWG8&eKfTYdXh(q$R zsV0YO=JzO*cGSi#5)bMx+O!_0$8|{-d^a}dKjijuy!qQTS64PXHPVAv!e$VK*uBuu zR;N3+sbH3hR-W45jEAEw*@pe%#fvYrPo&x-qVUO=QlsY7{#UCLD2)=0un80h_y$X@C7AZ7?_j+e=;Tu zc5YpZiP?Ff#3hvSGGnLan<$%D)QI?_yj8}ohyw|sQcopTM@? zGc!JTcB%AOlh^bb_l15vxRh=!7c#S+KmX9x?ud`T!@R_;)D*B4K!!+e5{wMw^GIkI zg7oV!Qs($TYU`)VOQ#tC4?+N_tUa%E>PDvbCDCZ32k}FIqh?o6ME}7IJ|M-6u5-5p zhfk@5B5Wgb33%YDQ8{u3n~77aMvT|sr6P}@9uE;%PI1TPcofjwGp;`vYK==>nZdgb zXEgNMruFvq@uC&#{!&HDmb;x=)@hgtGA&&Rg*WqJr(4Er_W8b&sUZ_C2nW+bb5)kx zbdIgDrNvUrK4n6Ae);AAS_U9`h2eVe8h`ltc>CP8ojD?P{lEuN?P3{>n%>5Q{&pS` z=mvO|oG$BO7e0DYcSi0wGDxUrhmS{1NXR!9Jm6g+y8z_?b<2DE_O`B&wOF_C2XqK- z*Eoqiq!&a_&6T?}0;LK?wSA$&n+t&Q;`bW4AV9i&fGEf2a$MK-53Na@HgY!H+b|0> z4agd_6`VB~GWWAK#9Nk;ek+^FN!=f_>t+%l-bFQZ ztAPH1{^IFz$7Ww)qa-kogG;KAW%+~l=pDe)TA;>^{iHYx{3YZ>L8RTxaHVmr% zSqXuQ>ecbAwz6CuvGoOup(d;b1`;_y@Q`@46r{z;n-zC^dQ1p6y3C6!k?x#XW_{HH zrI=AgnML5E_+Q89hGEPVe14QjT6(W73TS!gZXb>RdfGU6XOQ0&sgmCfWhS{?k4)Fu zm9?dGgGHFAzjr;pUluYT4NOj)u3&_n(3VkCki!kZM~`Yvmb)P>0bAF{Q%kL`KE+*^ zlsaN%7}Z=r!)FzaaqUfY`5+&FIPY;g6I@MJ(Oa?W+QYg*anzm<241$(6{=m_Y?NzU z60`tm8Oax^7JKC-$CfABq=~&|q-z+FCJCxbUA}qqruIW-lFQI(Wo@SLnwxXWAJ1F# z0xAK08BzK?vZ!!-i^Rl~wNPYd^TG8w^1Fe$GQxeZrax@lb)nr^Foay2wQAP$1Jx(c zqap4d`Y!V)x6y9K39g;&vLsGC{ZDBI==BuQ8UFgk-`%|jwbv!^BAEF9K zfJorvJu)xy@|ZWK88>{ze3t4^>>-p9fY3F}h+`jb)^cq3SJzkmx_DH3yi5(Fzz1P1 z5#BpCG1{ZsNG>Yc2?sC4K!08{{O=sOPqN@HPKK^7GlWCD8<$Pk98@@{!3X#fTwQX_ zB=(+79F0fJA`PDu?wt`t1r?*L>FDTm$5$bj+F?Ct)N2>j?fMpk zwZ~!!aiHv>k-%%iACM}5gq|&Hfad-1XUS8w2evv25h>faRt5VCJ<9(?<|487-AG&^ zb}7lBgZRe;gNoPpT6ef4J6a55!dqH`uafqpahPvjg+aOFNb??#gP46YCSDpI25S5H z=3br6m4|n;(pE{FCeXaqg-Zux@+JCJpSLhkBwbO@qv^1Z;P*3seEQPj{!5WeFKxT@ zE!OI&>aLOEw#<`#%d*V9K@|N)QD>+T#F!X@?-m~;utU*IXM=7L+C@FM5*_vI-v8|OB zB~@7{1~7Yw(e;e!jM|8WCte}uN;we#K^3B4@6jdQ zu{JC@*#m2Uwrqtf*0b1J#l?sr3U4g_)Z58R&^aJiu-gKEpuA`T3Jm6n5dAkcAF1tMUb`_cQ^kvLeuxsa5nBD<0Hu0L%BP=f1czA+aMqZ8WW6=q{c&YtJpHUz06)*J(;0tf+)tI?S&U6&TkJXSI) z?3QU0Yc!cq(q_lwxoTc9KoHm;>^U;)CNbgF)YB8DC1^#p5hcb2 z*#atE*@hGC&II7W=yi20=$GwQ6Y$*BQ07}Lv9tySiz1qX&v24QRS_nr+1C>64mtYp&>-KRQj?+SWtr~@l8#gyrV>pHj`EPQ-0}h zXC0C4U2TEJOcZ$Gr@h*2t@cla)(q&i#yr-oz>9&pEUP2lq(uU4caFi=T1QSa`N#(b zQMYVj@pjD&w}VCPvL@jXL1cltoZve;wNiwLw&&P;aXR28dQ%ogdlfQex-O#s19iDc zgPLz!(10|CIc-nW*!-7RlP^HwbkBI@#d%z8Ox4Bq@?aM9YztdNShdq7)E6 z-C`nF*wNRF(G_h$R!A(gw_Aatf=``&g(+SEdJTXN|LBY9sDgFC)D#MpeSLiiMx7-; z(Ds9-BRIgb@#6WGL$3&-G8WJTnFFiAckG;#ITS-jSM54{Guqk^4HL{qG6khh{aAcWwiw`cp{?&*t+>}RC~$Vku$_fu>QV{*`3;NAhVBO_sDsdek|$jGucJ)zBW zz#||T06KudgCc>CF3$|#2VS9Sm?aj3L)#z}sVK|cJI``H%6g*MJGC=9Tch`i)Lj@e zv;^(m7H+%DoPX+0@@28BhTi?(BXsWR$)nwMK zZj(3Y*=VE?L81vc`Zpqjk%go#%!OpF<#lZA2*5g)JHTk{ZEZX*UE1rL`kU|d@fHb{ z2#T-)wewpa+@}H<=V#9L-XbsAJ+TWN4mK8}c2IjWg%TZ%FlvJN`FUrVds}sw+wcBs zC?fciUccUEX=#Z{3uq!9#(ei=>y?`$Tfox(p}nOD@;@UZ$0N%{n)7j7Ts1hu1QFRB z8}@%!0{yUL#hE9^JhlVo*VPl5+5cH7$T+t4*Y%>)Wug$fp*4Y6Oj(+<=h}T>t}^N` zx7+@RNuxnWVE4gH{I=D>BH```)j97|dsG!c2?3eI@j-HE9&u`#h18>Eprt2hMx0LHkrA6^x%;*2+|9`J9Qf~W2Y_|}*ONGXLS#F5Yvs+av$tAGu%~Wi z!$BD4ueA*lmzUpYJntQ3K0b0%!*sbh8cmqCsp-{rC!?O#{;HE~W0GH7r(f3v@Gy=g zm$cSIo$E9lJ9O-_+X+T>9CY+1gmMqEr8ffQTlBqZvqiKMV`H&ItKhlbu+K65nx6BY z{{U#XeD&%nfrnK;empVeIM1q#6?(SQ`j)_q_*I1+gm9z~(RU{{5J2>h8=-sxqjdQt zo-GAzBLNR%W!vWjJA=AEaP!WvAa2z6?`#Ca*WP}r&Qn$SS*&;9w--vg{4f}cW*;82 z9m)=~OR|Jc!R*j~#~z4dW&U`=6lbwEwh>nn#uJ{u4<)9LIO08T%T&|Dt zRv#$Hf&@ccVIx`S`K)H(uXIpH$z;CN&tKKIWTVWvP~;C5 z1?$a07^y_O8Y#Y=^2e~l6E_e2_ki@}*o#zdlBWaA+2(Ee!Uihyv$(_yz1Mk_wVw)x z7+d^f*rfm-}I{gmnKnZ!*vDv2IbT< z8ZGZvS7j~o9&^!USwp!%XnmHKm*Jj>sul)oIPP3=85f}Z{j)>_KuBYd2*w3OGp&Ex zJKwnp_Bdw@2nve52P(S1$_q+Ya`%9-h*axHbi>Dnz6!6zb>elq;B2IBkGBTVLAt-x zZ~Ws!MNXE|Vw2Gh*rTMc%B&Zk18AzIAq9tQ;)Al#0ckuyQ0S#=^Idy9daVxHw9jp! zASaoLo#FpLS50UEC$i+VS@}YC>FO!(dhm3tMu_!}5(Kp<;cx@Y*<7;AuJe+GU=(?Y2_1Es$G@=c!1(zLPQndZy#(k{t*;ok-Bt5 znbgJ~vJ20?I~ye@7v}$P*?{iO8<)993eZ!a%!M40J-zt48S5QN1pxlf7(GCouOJXwFCa{h?uey|+Tdf$HFzbSU!g4!=t zfe#hzKf;?Bq)(%H-f{=pq>X9}KiDmokdw~knxt~2yc;eyVO@p%6*wKhl6(;~5nyAJ zPf}YgYz|Nmrwc%hoXn@5y?GUd8%CP2r&WseRe`?`Rud?Bc$661zWaHt54sefQGPjN z^BdG1k!!I*zD)TT-y=9q>V3O2RTfX~-I?OD+QhTQ3`_`+6FPvIkuyH*Q||30_tjEu zr3S*y*EsVsUo-RdW_f|Y$Day-zlX=hi2b+7D*_zb z91yOs+i89V+j71B0X$tf!IUEVnf3LybzA|{juM^r{x}H|QL&-a8I#&85_Fqn8?KJb z8QzKOU1jF+}q z37`c(58~|QURu`auHv%qWrYA1FdT9@LRu0qagr}YXcOs<_(%w!V5BN_&NaM_ZAV@E z-~sce{Cn_*>xa|Q-|lpl*tCwFzHKS5z{Qv32y>HQ=zp=9~DeIoIlBC_*{p zvuQm7b&(U~9S zdU+9C=x<;C^4a~y6!5W^B3n@E4VcJsL%fAn7hx5skI;E3c#AVL1Y_QjKy~ka08mG$ zmiFNFLaiI8X}E*J@8~aPMU^`cbH0-}ZL6Y1?%`}WcCKPs4>cxiy2ePpmlE743F!fT zO5x~i=DwCQ03j6lklcYC;}N65#61|m@bZ1@j<0{thw8O!Meq}h2=Nw`zi-mt^2?DE zO)jMEK&!MbnT<)}%bS~%k+?f0epd1J+veuw(=8-p$R3*{VFktklH1esiAyQf{4HIf z&8iUyV_>)n*An!DAdUb()acxu5WX`=JEB)%9@=D}0+1gfz2}TQe*9SQsOZUO+9hN^ zu<9fX(hj5o84htQ&*cv9sya?kyeCkINw9x_P2zY7CAbNfsdu8#5;VVO`F^*~gA9Wd z421G;1_G$ zD;N4pk{Zh+4sPy`=t=OEq{9jV+A4KUh$(ABV5eca$bJ*TD#SYjkv9%E;sO7I!o>}~ zRF|0%qO>co|07NPGEF*P8&}=WpXNmO7Eecmvm7R=8Xp(;tV+lv+pKh#+$3Q1+;(uM?eWw#76<+=ESzo3{b;b$udeV&!|eRc52Npvp8`(3 z$P_amjS*JB&4|`$d}ZpcEwMON^uk2U)M?ewdjP2(`bx-!3M``HSJu4$&d);|%>VFn zY|j3@A<6NQ=opbt{%Koz>8g)QbDk90UJYRxbcKjCs*T_~(C};yGXK8Pg5-{jr)6r& zh>Weu;;fxxjvYpDGJ(bWi4}!|F-iwxDC+Y2q#f$Vt^U1S6tihIpz6>%AdB`ij=lIz zfoBlaKDPPzhZc3~+>6T-$~gb1A_;|NTekHPh~7ZFcGh#X6>kSe2VerF;U1%qQyDFP zXs)Jj!v4ai2tv?>XtAbVcKaP59fmdc)x?Yl8Y>VeE0QH>BG5c1jVG0X`9fp=eFOjh zI5F`Qf+PA(UA@=gN803cYvV-Q7`=)v%w&81cC(B@97`uf9NgG=Uv)c=56 zLvJeH2P#=5{O>ZtBkC`Xgl#24lB8h@X>+0wj-1?Hf3=}0 zk7ZokFTc?9*74bJTJ?k7r$D@fjE)=LNCC<+-;g?X6xv=C`rw%nNru(WSu$|TQsV?* zyZj#x3PRu#gjs=PnL+5)AUd}*`O$cAClV{;=@v-*45CtHmQ$<%JqRz)=80@6>mvct|XF-iPqsX(eNO`o&O34(}O(8ptMx?eqhP64X*L1BDdin|ehd zYyd|h9H$RMj>L2X00-qA1B2-EiE|uej`=BIPlVnW)?$(kJZ;UcnJ;~0xu3@^lF)df z*u!r<$NHCpo!uWO2&VUQtgQuDFnEmGJ5Xtt!ko7qHI(~km*f(~>Q>{xYJUT+dCqFi z@WY*RnH_~43>63+QfDGkA}+2Vz*UReir!xo+xQ~}WqbD0+6WwCGKamYm%HIFTAjO- zgJTnkKrAqefEVE~{<1VEuJwZ7eVqYm1|V#}O)y&VJI$n|bkp3Q&!xQ7cw+!#A=pC8 zQVrV>1QP7Ep@nxj!iZkn0`4F%C*nu$-n~2XVm8e?>AU1JtS?N38 z?FD$y7K}XL9?*Z_DzO@p0g)SUevml=wCAZP!HFQO2YE>7g+mFY(yQI8F0xUHbtjUT z_*L_HTAaC1g1Cs=KTp-!hsTPRh<^Xf<|uL>+%TM3CJJuOCk2_kLoXykudZD!cU+4n=fE15XJ4!_p5VO{urD1O3kOtodvM(T}Hg;!_U9c z?%oCr-@&HMihPN)_C~klRFpOFJp7Rcmn4}F(E&dUS!=5uG@wx|ASR2Q$v&tLLOstQYuBNGG7~Y3H&X%-Gj2a1D_fd0_$%coR_G!J zqiTVm{Ehu-6pyyIZDR2mD={Ty;`AWzd}DaX8jGUJl0gkDm>~Z+{5KY}sHm0v2Hqy~ zCQ7Q}ai-aH?)1W(l?nUDd=bzuPy=Eu_*y5DCO!>n^t)2E+XWuz2iK@Qd88UaVHgFa z#9b(SzK0ix+=ZuM<=E*{*;7L=E|rvx(xh#Fuu z@ehdEGt8spn3SnW21>a$abv`znJtdy$0WbqJ6CvwCfCqcA4l@Rtb;#%wTx+K-w!xK zOAE~lOi1qge@#h-_!CfeaJ9B1nY`IPX-HG~LM)ox()1lUFGW)zgkSnZQ%tClU2=S<7{KqB!hbPDkUpl=jTKF8ZHk#NEoD4O^@>3%t zgdnL}9D*b{iPK#!_8{jkQ863|JAv^zxPxHA2K7x$Oh>`o#+$oCZlCh6h%sBTT~vOU zr50-jj!|z@5oAYbkU)5p%rBvV(R?E92TTFk*!G=*kRlp7=vb=7^@>LKaEAw2cf>L< zMG5zDd46pOEKrDm08CS@mS?@k(4xj=zgKI#2oB_NCVMzq> zC_m6`*lUO%Ia0K{>j4oZ*@U~qn5cPUG&*l?+8>)1nSEOjh+VI?o^!YFgMXSygb#=q zMf{{uZ;&u#wfQBh;OOx1OBv3H-aG+g{i@ZZB)-xCuBqc#{6nGe;;rhNZGVmKnzP}R z3bT7ouvUwUOGpIb1EI8lp3l5p=2ArG{HNXFl~W5quPiTLej&8!QgO~XtKT!4oKsn0 zrC{Z(4gUuU=pD<=8(rp-zE=6Q|J82?ebW8f0~4Z#Q4@CQxFTp7a@jut;RVv-V&52F z`Z1Z{fImr`#*ITbEO}aZEA}STGGAYk@6X(9Oz!8)HH{+OdgR5|+0t&ZR@!AlOjQ7s zgA&hPP^7g+1|v(*x?ck_qVYd!tfk6t{P7^Y3`h-;?awN2Y-6S%11$_J1#L0p)157; z|GItzY9WZAcCpbB(?}u7zC%mNTh3OC zU+(wq-JkarDc5m;k^poYvY!q+P<_>ZW)sWx&B)&9d}3izl6kwl-O+e_o15B~Y!qwl z-73XmG@5Org{-b!=+-T$RS~BqfZ>A>|xA~>6<>lA!`ce3$*(kDpu-_km z(jIBA&#ipev0D88zvcrx*21wY{}nsycorR7p!u!eUp{NY2$wc1nlf}Jfari~p4;F0 zXMVt~$AMNLWEV0Uvc^73XR53=rbCfxdOy}xh&1z9vdQl1Rnxhr%8(5Y?VlR9<9eP= z({LRMQDNH|`;TQF3*+v`+ak8c^0VF?j!#Ui0ZP$cDl>Jxh-B72`d40DxV{_qze$9=DI#t0drm)BZsLy_~?cH(gX1QM(b+Iw51E*TRxaNxrnACw}(fbrs3TNaaQ-G!RiLl~u?{Mn*=7Lb6BP$VkPdVWeS? ztdw08H;On$NGfs6WMuxHZ~eYK`sY6Gy`1y;yvOVHTrWyeRu(%H#j3E^Jqy=}OQ{OU zrV%GEgpou--6osLZ*(jLALV$%Ckl3F=q8rrjL72%Nk4n zI(r_nZ^(gn$jbVI`Kya$2CJ|d&?FRW`BE+Zx1-S@ca7j9!rBMgK09nNid-y71Q996 zL{?&(XavZC`epZNqgci!Vw!|)fC%Nyp1bDlG{bR=qRH@5SEWe`KSwD1nLFg{iZ$bxs^_|_*oNrL5gSbFF*sUyncVO~2Xgwg|!xZ_3e4dI_ zkG-TT4Z`CaR1XE#VEGfmnfJZO+8byJLM_HdgTjDvY_}!JB|ruz#b|@X--TC{In1b4 zlM4y#2d2dy41iu?h7a||%nZ^BN-p-%ll;I7v-|v;rN$O(R2L3l@*7=COUveYfvl!N zs5qXNn9FC?2KaP;IM+Z9nAq63Lr!I42=y8{=l@?v7W!WH=F06PcCnqf;|ze z3W>al%%vA8D1D@8q!#;4^7flrXmCJqyvaX(a$x!d$)ZBOUKl(tnlXq|Mv4JN-@KnM zeGqD+{QN^`5+A)8yf#2X%Qatl2eBg3U$s~@4X^+v+~ueRYXjtw=R6=!OYKsbuQoy zVMlGh{N5V2f90kwH_3oU3DwY)p>NX=;YylwNF6?rwhpY_Dz&8jC$6E)#MXI4|K>Z0 ze)uL)2Hn2^rb>&n-+{yg{~HA83T_QbRG zkIz0bTyJ-*G|s}*beM54UzLgNz*2`ly8yR5W9$V@W~U#*2SPJxZKH(}`EE7b)^D2W zwajy~@@~C}k$V03{GrFj&9l9ta##Z&(g)QZ9}ggTWgBH9$;AY&-MhxrO_)M z6$1}?Cq*_!!N&asshF>D0EYGczTaErygcp9KzZQ#eFFq|3w^>%qQp)_l1fO*oh@-jUR%52`1P0MVxrg?gr1f1v zJh=<4Z9B=xtH4~WGavY5q@;!&V(p>kZyE@q(za(vIx!kfjR18BizrTQVMQW)+^m8iPO%1csp zF4!!8q>B{r=QF?Fx z5gUxCo5ivB;s5OM91y~1L$K6wEiL${hyimk64?7^s_zJO9C3^(fSt&|9k`z_P7fR7 zdWZWDggSX>Sm7(N9t$va?(Okg_90uBgf5dr%s!o^uR7yRc)Pj=t!Jcv=`8=!0bUu( zDcti*ZfVMVS%JiG{?FPvj=3;v{iaU&T|*~Ff>9uzruZXh>3r^FSmTI zHRlUPxJsid^Q9I0uKDe~F>&tW6WmATOH(*8NRgKFcVM}A8Dko)nqHtG@4%H1k)h0-tkdm(^%a=*ZB&UM< z@alDL<4~k_+wr_FH)qn6j;qmG{HW70)kvaEdj2^Uvr0oRH$OiR6Bcqu^G$t!$N6m_ zml{Mq69{qNhzCI=9OB$nMD0w3Ngf_G1Ct$U6#%~}GyJwBiXYMtENk^VqfeKALvQ_K zY^)AG_v>i%yoXLE(o=A|L6Jp)3bkS`qk@$(`)NRhS&fVAR}7IQeo3-@DHLF?vkL>N zQSjUg$-Eo~QA|fcs_!~N6cjJqAQOLa>64TIv@qaAkUQF2iI>uNZ|~WPCiGF`tvS9t zh#@ULN&hCW1SWfP&>JgFot>8QE9EH1pxUh-) z-{$Ov8YD2F3_;9d0MeYDjlR>St_p1XQ8%d!F%%LhLv|oRi71XhTK)W)zfZ+6M`tPw zl^56D6bV#V`n`lJ!TF6k?V7q}OQKiV8OQ_LcgF&A%rkOoa;yVba{K6J+(T{ON=i>l zucV6rAlQL!{hWLl%Gy3+B_Xpi8T{s+_=O={nUy4)k@2P$a9%>?gC3aVt%Q>Y4KQ^% z&?=9GGK+om`@D0F@ZtwyONfI>c9ZXnhu``+WU-OpHp)!?S=MWTLO+H_MheZ%-)TPwObOtI*t!G+p2_KE zO08*;-+g1PsMlNB8(PN%&?+HIqOFbO%sowKZIMs=eV7MZ9bE!)Gq8Rv8ahj?9ZBBV z9zpyR%BX@NaFRf&1_B$Vrvt+#MbC_Iporj8ZK~m+F(f|hUU2f!49A;*Qx&Nq9~UPM zp}{(`L8}E_8|TN>54hCR-Bbz#cQiCKK&C<&z7c~j1+I?*+>G?j#i7zDU*fH%GI0_C zLY^JAQw=|n7EDskXKQzt2Ouz=1Myw%8CzX1{zPX^ZS8&yDF$vsO@AKd$fhK9qPk$0et z4bG;4il~!WsFr~j5MfcBiGCUVSvfg5PQo6SFHi`anCZn3v43zsSh>0J zAQcXPDz3M&@_A{-g(F(9$UnAkE_305Hv%dx;B3%jKh)pWF|hkYJk;1#RaNW(G-$Ht z2ar%=WM+ou3I#LJXsE)uuis{5q!~ZrEYA$Jd30hmpjIfN;$x;l8czpDvqKlKN3a%p zFm@)~$?tLatx|)*(uPhrL(X1-_5-$LC<0fALg!_leb%Ns*n$8e6HN&@MTrO+2oO2| zECxFNvVtN|ZV=3(b789?_ch~18NS)wev`W?{yNaeQ!IAZ8fAbnrd<41&*M!q?X8Av zWW_Z1Ctm3aCLB#*3=^>?R7?m|1dwj1mfRHx@MV^%SlEffISAo40-&t$kFLha7uPi8 z1H`rIxB|PFB_Ap|;ynRGStz$w+PwTy+<-KNBJ^4Fpql;-oMv}yW{q#38xk}IxVu7iWMe0*1$7yJNdxSA#rZH8d?3N$4ofJ(co$5J$+u!QQPR+ zSIr0@v(tEp^d&pzv+%ufOXEO7VwGF`$R7`*zn9xZNpL`UFJC&T%Fo9q{@#F|EN*Iy zIJi4tM?>tfq777B2XgB-L)V`0;6NrCtfF$1{;@u_$n*pj0x}|F*a!691XQP#}LRLTEvU z(G+8kZUzg!{cgFF!gDg;2~H!1@GjIjk^K^;*P~gH-9m~nfC(VxE~_4=riau{Diga3 z;)Y)JPb9D}XvPrk3p)w*hk=2pW^-irBnhqa_o7pw^<}{c%f>Kq<>iI!C;BZ>T(0Ku z9gDzo#gOYTw_QeN_kXYQ5K8yj%7serTUqv~1=?R^O$6H~;MRo}3Dpsl3E1x|M$~q? z3ZRjoY4@#}y-w`M>7}XnqOW3*5iA|Y=v38xk5kMP>Qp))*)@Rumbe z)z^$cv5vyJ^?U`j^Fn!>0}R3SilS78IwUUtWEH9oj?MyicTw<^%OV4qkf}kqZ=-{ zYw#8#EVy!k9?kfi7vb{HrV)CY9%?xfP4qi}-nUR=B_jf?Is0bzH$5iC##MmHAjt!s z0cSc`I{c2C02!^`W{9vrDFCzyl)x-T0?MkWls{A~0$ha;ib@EDA)fg_<9*0VQTF5a zx_$5ub3lQMDi~L1HqHJ1jly*rRp^4Wcv(chQgmEB9~=fhfYS<#4&;;rHAm@4W?&D> zI_QYKRFb1!TcKe=la3ZVfJ(b2!h+wA0~d1aT1;;#Dmu_250>h*^h~A5c?=Dr=zb22 zW<@y(z3qz3ddFPP#~&(Ut!Hg2Tm?h_+-&e{HS9!d02yY{LXT1WXlb_d2C20gf328p z!L;Gz#q!i*2otP>P7Ty7mKkIqU3G`^j9#JW7M`0liFiVg%DMk|dS~8S9%F%^eL6o0 z>7{)Gz@LFYaB`k}BBjo<*B_Za{`n!?;E8H5DZxznH8EeTQsV+ZSdt@T4M9D`f`4Ht z!Igm;1F>(Sd~5*fNxTMsgvz*L#!99fo?h{nd4$YT{1GVC9H9M%;hwT5H%I8h>>)P= ze%85GrW)DdP?NBf&!~vq$DJ3aj<3@|G)Au@C09;?%0vmO`WzxX6C(~u1del@66B6~ zW{eX8WN1f2INk+t^rlJdgHIn^d*(4T7vv#szCwnWsh_(3sDI^4fRsR>$QPpLE_Mby z)C9E$Mq}ALA_Uqvq|}I&N#P;bv#ch991E>gT68}%@C7b2bDnCk1Pq`DD{B> z@VLZ?L%0OD7cuR{EKWj(IC3=wW5U-$ak@AFMbqxLvSfo)8Q+iChBX50d}vo;JV4g( z=!M}$PNtRmXyLa{bKiyTb0+aa{eHC6KpoOL-HAI6o?|U#NIQ05gObeD81e4iAULC~ z@{Baro{`*STkOco?aF0*?RWpg9`-}*lO#kAM%5s_v-n*rI=-Py9&1Q}K_=VO8V4wW z?N;NebP*(WQl$Oo&j>;$Iy<`u0c#RrIB_8WYn1rU_Sy6@!%Sd^mtMkGOf<)S!O?|E z+TeXWd26|aBXyGC2*AznGlPGI!FF-y9e{{R&oUF#5I@wg6SNwZ_WSp`28oTP#vTAj zS1dueR=it$a$^k!&e_;J`m#C#mVU=wqMS%u!Uep0)fO!7rb%SN==bL^k)XkXh0>4+ zD8M0&AgMZO`qh|Up4$hVo3rFY_eGCOBNb(+&5WOPmnG2@F=3Qhs|7I-&>9dd;-f~9 zFo=re)jiYN{DRB|2KMnXqQQa}VUa>Ptfexfa8aD3{RY`5lw6&1(DIH{PqtuQ3)TSl z;mD&_)74Btlxp1F1Ba)stWLXOFj`xGxrqLFW!dpAEL;btQ#Qzul4VoOX)OU1 zgbN&u!1*RW>KW#RHWEMtRXmdG#5LXi^0pnkD4yz)RbdZTBahjB>Utw&6S&I0`|m#l zA8OG?KQgRjM&NYIHhWQ&vo>hEb2)yq=C_&oWQnkDc9#rC*``TcC=h#oX|iBkppeW- zZo*G#AOb)l%*tZ}>4l?bNO&W= z(=!vk>j$f|?Ojz3wHR?vFrR|%3+5^juJ%bbXCU&L+*r%|IvQ8{ETz&O9EHfeitHap z**KyZkvMWe2bnF7zo9%r{TX7p>!V`laKai2z(<}%=~$a$r$)qGoQ5b68htp3a$ml@ zf$19U0kdFQ7%1Dvx|{aDj$-h36b`|W4n?lgtSjdBrYCl3_TvsF zZDB`513GOm_Sk{Ic{g4Wg(%kb;)W1Qk6H-Py>>q5*7&Q+0_f69BdN{?bBxG>o)+UL zUROal`W{e!WIyzdj$$oHU+$t)r4S2HjVzB3M2d_QBV*&VA0EHGRZ6wj=4%t49QyM1NT(uaHmoLjj|1w>K< zwz`^bpScb@^NPw8otDkiaVg9)EuDqVf}sS=7DRc@&STly+3>s|jf6W39VXfo%%!e* zV1(Wy3EeBgnxR6&8F&5Qm+}UKy^%H0RsuvK-y(%6t7Y(@qhrS6UN{xa1|$-jPRY|T zjaN?C8S`RMj<}=!z|uq=fG2Vy?I5$9s8SMm*rcuVBxs4s1O>b%Hz4H8X7D&oPkq5w zmp8ID$2&&%&`upOkywA?Zy-v|NgIEqHrJ10W0FD!)OwAqz!p%g>KwZ=EJ|D73|c;L zGA$egV27gx28Q~2R*){c*w5h(w**0quK}$;gh?^tfE1h*>2g~A6Yso6u+qrfa}uHK zGbfKD1L3*mSbE zeV?t(jPZbSi~NpVyMU2K0fivCssYc`mcddJ6HN{#1V_o@ovp*6X{HcEB0AX9n>U{T zrNcfk0ZyC@An5i=5rFOneN-8|1a5wr{t@0M3+et{{Vf6o=wv z`JoFzH7G?p-BOUww9w|Uw`gb<%s7c*(3NOcMHV=UBvNF6ZUcgJ5<09iycQsjhpW15 z`)Tf^qAjsh&f@qF7lidZnKfYSJO8SF0YRt~;_5@qKp_+hF@n#I_m$S?iZLG+y9quZ zSX>i}3eIfz?N%_PT)m~>se1TDRCKMi*+I#h01Pi*UBkn&H8?t`!Q`loRwbCDu-9UE z&A<}@Bu-aa+!tY)X8+l_`6X&C@Tf{xk3Kje4uC5N(xN1I!|<-~bna`TQA2chy6r4B z1*P)K7ctxtD;a79&qmu!O%D*qe?$9J^;&t2<|J9ljwuUm7cttVnVr!lkg>aXiO}-`QM^LWy)I z-yQsjGJ&bETl*jUDx`E_?1{Nr0SZzeUGwoz08?l5f*ShD4qPpHD$G)4IR4*NEX}a{)eZuxmdc2 zssF!t_lx)+-srgs4LNlPs<=N$74(_p`t0?rP%JZaPi`7`7&sH`L~mJ9C1j3)Ug#}2LK#S*JYK`OTRCf^urLT#H65L(`}gxUNWL$|YO;&p0Jsoh z7mDu*yP{4VYFupWaEK7a{c^-TsAVPA1EL;QeU)vZ`k&H6QdSVi7JL2^NuU)&A z|LrB9G{AH4A>+`-VW-g2P2!5=lkTrt%(PK+Jo)cR9NLr)*DBO;D>yb>)eSq}wmsX~ zyi$Ao$$y}XGX9IlUTmOlwfkfJ34Y1_P|H^4I~g-IxC5La!Qc4lShLf1g8cmXaywKf zuIqfp>jxZ2SrIF@Bnbn}uxGS)L*9W~aJ1Vkn@BhV3*>#{KrSj>$FknU8~)*R;K%z@ z5!RT|iH$)VV(5me1}~==Nwqb4-f-U_8e#Y3Rvt1cHDk_ER*Qc1N4E`uDdm5#`Ips5 zh|&Oh-%;J#Y87z|%~01!k6tCT#lVrEAY)$i_lgffyNUZc;c*|jPr@#*QYk-^Ry z@LYjDagGbOvzO@U`G;nfnNR=rM&FADMd8$wj2`&XWUk+MZKZ#~Uyu*g4_JRbKEB*B z28T2biDNJRiLQm6*yHw~UH3w!&Gsi^phL~kq(SrzZU&L?Fto$HXmfg4%yGkrcFrBZ zPVU_qQ)pCBx54fX@jsNRc;{ZdGk38-l68@SEM*#a+!y&kFymGaUT(JFA_@YWd(de^ z@q{v4UP8&5aRCkeIMTHKq4)h-Ne*vl96@oxsRkbvfKZ2_zQ0g-*~`Me)-h3GCM4I&j${f1cT-+pe#BPg?u zm1J7SS>H4wRgcgl>h!gJK-LZRjv0cOimJZI+Y>2&H)J|yU%y_+Mz2hYtd=^)@6K|bs@0rYabH4<)7lQZAEVab}|~F$iTGtqxV(kOTj^eJ(O2kI;VTf z2F2Hdfj@3cJfWkHxTQ~*j8|?hySqbcE%xT^cSTH1lVp!wlxa_B8nD;ppB3ByZ=NPM zg7CoJKvV*OB)COH#({g32!k~S|KCTxC)JbM$V9`{jJgYADTw{i(is^g(TTY=Y>O9d zaDBJ2={8m82*VDx{^W2j>d;y?6q0w$m^1I{V;n9+{$Xf`h{yXz9vUcghe%07U=dWy zckb{I!qzSE(Wzs{#Iv>l`MjQ>AYYQ5oy{(345f*_!D}>R=)PQ*7CdnctNk&~(=Nog z8kWh0SW+k-&aK0I1CT!$X)C`!^#Xc@E=}3O(qD<>^l4|Du|z)i{IZA1lYao#xETq3 z1Ah)b5xO|``i<@H1eO8RLXrysBaRm<2xLc}{eo1t%sZcV`c9z`_Dn%RoRbA(IzA5a z&hUv4(TRZ@z!z}urAWx@aw5|Ym^K9{xf{|L0Uv4Tozqqu;%%A)-MU9Tr!-nVpQ?DjaTz%4qTh0q2%^9ElDFev*f*YC5!^L+FH{$@{_!9>~GK2GmK%L^MfRi9Ien7zCNZ4^p>TfDrga1XxBM3s3*HC^c6T$^#x|7ner~yyxHoQhcM3j{Y!96uFiE2FC;Vf1!v8jew zjxWuR5tj#qp02L>>GOzw;)JU2;zh3T@Nn#MiH!ByN6OxQb0wNKRFOC=pt?p9!SR;N z_?W*6q*0gU2}~`tY1>Lk$AvS+aj6fTMwKcZ_iRn`dRE5zmsePB)0ub7>RtD2FPmmnTgzpS9FC@aCDnLW(bbU}eE!{yzM~I)l8Wt@m!Q{0vOd?b>*5nz~McH(u z%On?;U&082#+zR&<1y_7jUYM($mOv1@sXr>Gp>%95V2HHd&E`W*ur?9Hb%(Vj`$#_ z8#@py1w4`k-`fH=|fvB>n0*YvS}yfbW&+JGn8WPl{pD*%rlvhT~!trdy6$l zlE^9E_vM!dV}$+@p$FY&E4nPdYQKhU~6dCy{)Am$Ky znJNmp79Npg{cUP`>&T9;u>=y~$GLK^+_n8YdT7he{R$`sSY8-6Im`K0^k&1oB>L6j z(^ITgKx}9+%p6x~@Snm7h^qo=mXP9rZ=Uu+#)F-4ta%|#>Sq1wKnixjcfm6`#g7f| zLJELiqVr|nM$SEv873B<8MX3;-FY~zkZq;JQeuvN53+tLt$XtKYSF@-oXeP=Yb}#S zbrT|%dgw7W`YpHs8||Q*#c6`S5C#JT0IrV2Ps~$t5Fi_up*>sxa>0?UK;AmG?jS%q zEObCSflU!2_3%a$z|Huq!;xw85jMtP;ouj2%ae8|qU+7u@C$BVoYSl0PrK@19q``QQAX+Xj8 z8IZs)-RtrTg%beR+^2;pXs(H`3_p>?av;bK{j{~?d|oCt3G$JEfqu}E2goMdS`Qub z6I2X<`CoM3Ws3Z9=Fu*U9a(X$;rIwY4;>$nNhrP`f_5{7)(wdi_6eM)IZ6N$VPT+C zfQuh(DF)PSpK0i{g%lKEGp;pYjR4foZ~rUu(8E8rTs+6=X{0^AkMkFty>t&*rf1F+ z#TBdme6tB6IEp{ay!VtmI3OgTjBaO_zH^k*tezmyf)~9Cy=_L=OoruD?=Yp=|mK<$SR1Ut6#HrCd!IP$NS zoxybn@LLptEl2or6K;M&vOE5ucK}8W1PSh9wHkzUh84BSvSHJ7G!%P2c=CN?swkPQ zP%PI-Cz9CIMctOs^9@{KLwN;Tjg#@iXD4nL%S7q1h;|uDtV_b+CTn|$70{U>ICr@l zSPSmQ9|bFmv+p8eHCMyV(@0CrmSwxtn{?AY;qvjZ4<)8+P0)vyE*C6CG!SXaNwZb+ zIMCT@U_nEX(pB1IYs|}5qpz&e#@9V|#m1Ny4JQ%_x(du7{hawl>jhPY4Pyz1Xw5Dq zK@KHdQz>3ODc<5whO}p0tJ6GHM9?At6K#A9|G#7EdT%j literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/minio.png b/doc/talks/2022-06-23-stack/assets/minio.png new file mode 100644 index 0000000000000000000000000000000000000000..a71e9ccc8f7163583e10d65bbf45486e2ecb867d GIT binary patch literal 13497 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

    ?0j9IN%` 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 literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/alex.jpg b/doc/talks/2022-06-23-stack/assets/alex.jpg new file mode 100644 index 0000000000000000000000000000000000000000..eac0f0a949b6a60e0dc2757d789d548d92411b31 GIT binary patch literal 4914 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 literal 0 HcmV?d00001 diff --git a/doc/talks/2022-06-23-stack/assets/atuin.jpg b/doc/talks/2022-06-23-stack/assets/atuin.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f2fbd61db2095f165500463895e10e4525b31fa3 GIT binary patch literal 269747 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