Compare commits

..

1 Commits

Author SHA1 Message Date
Quentin Dufour 1be1715522 Benchmark skeleton 2022-08-10 11:10:19 +02:00
88 changed files with 2186 additions and 3276 deletions
+7 -7
View File
@@ -14,7 +14,7 @@ steps:
- name: build - name: build
image: nixpkgs/nix:nixos-22.05 image: nixpkgs/nix:nixos-22.05
commands: commands:
- nix-build --no-build-output --attr clippy.amd64 --argstr git_version ${DRONE_TAG:-$DRONE_COMMIT} - nix-build --no-build-output --attr clippy.amd64 --argstr git_version $DRONE_COMMIT
- name: unit + func tests - name: unit + func tests
image: nixpkgs/nix:nixos-22.05 image: nixpkgs/nix:nixos-22.05
@@ -34,7 +34,7 @@ steps:
- name: integration tests - name: integration tests
image: nixpkgs/nix:nixos-22.05 image: nixpkgs/nix:nixos-22.05
commands: commands:
- nix-build --no-build-output --attr clippy.amd64 --argstr git_version ${DRONE_TAG:-$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) - nix-shell --attr integration --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
trigger: trigger:
@@ -57,7 +57,7 @@ steps:
- name: build - name: build
image: nixpkgs/nix:nixos-22.05 image: nixpkgs/nix:nixos-22.05
commands: commands:
- nix-build --no-build-output --attr pkgs.amd64.release --argstr git_version ${DRONE_TAG:-$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" - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage"
- name: integration - name: integration
@@ -108,7 +108,7 @@ steps:
- name: build - name: build
image: nixpkgs/nix:nixos-22.05 image: nixpkgs/nix:nixos-22.05
commands: commands:
- nix-build --no-build-output --attr pkgs.i386.release --argstr git_version ${DRONE_TAG:-$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" - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage"
- name: integration - name: integration
@@ -158,7 +158,7 @@ steps:
- name: build - name: build
image: nixpkgs/nix:nixos-22.05 image: nixpkgs/nix:nixos-22.05
commands: commands:
- nix-build --no-build-output --attr pkgs.arm64.release --argstr git_version ${DRONE_TAG:-$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" - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage"
- name: push static binary - name: push static binary
@@ -203,7 +203,7 @@ steps:
- name: build - name: build
image: nixpkgs/nix:nixos-22.05 image: nixpkgs/nix:nixos-22.05
commands: commands:
- nix-build --no-build-output --attr pkgs.arm.release --argstr git_version ${DRONE_TAG:-$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" - nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage"
- name: push static binary - name: push static binary
@@ -269,6 +269,6 @@ trigger:
--- ---
kind: signature kind: signature
hmac: 362639b4c9541ad9bd06ff7f72b5235b2b0216bcb16eececd25285b6fe94ba6f hmac: ccec0e06bf6676a705d6a4e63dc322691148bc72e72a4aaa54be6c5fd22652a2
... ...
Generated
+294 -267
View File
File diff suppressed because it is too large Load Diff
+491 -539
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -1,5 +1,4 @@
[workspace] [workspace]
resolver = "2"
members = [ members = [
"src/db", "src/db",
"src/util", "src/util",
+1 -1
View File
@@ -1,7 +1,7 @@
.PHONY: doc all release shell run1 run2 run3 .PHONY: doc all release shell run1 run2 run3
all: all:
clear; cargo build clear; cargo build --all-features
release: release:
nix-build --arg release true nix-build --arg release true
+4 -4
View File
@@ -9,8 +9,8 @@ let
pkgs = import pkgsSrc { }; pkgs = import pkgsSrc { };
compile = import ./nix/compile.nix; compile = import ./nix/compile.nix;
build_debug_and_release = (target: { build_debug_and_release = (target: {
debug = (compile { inherit target git_version; release = false; }).workspace.garage { compileMode = "build"; }; debug = (compile { inherit target; release = false; }).workspace.garage { compileMode = "build"; };
release = (compile { inherit target git_version; release = true; }).workspace.garage { compileMode = "build"; }; release = (compile { inherit target; release = true; }).workspace.garage { compileMode = "build"; };
}); });
test = (rustPkgs: pkgs.symlinkJoin { test = (rustPkgs: pkgs.symlinkJoin {
name ="garage-tests"; name ="garage-tests";
@@ -25,9 +25,9 @@ in {
arm = build_debug_and_release "armv6l-unknown-linux-musleabihf"; arm = build_debug_and_release "armv6l-unknown-linux-musleabihf";
}; };
test = { test = {
amd64 = test (compile { inherit git_version; target = "x86_64-unknown-linux-musl"; }); amd64 = test (compile { target = "x86_64-unknown-linux-musl"; });
}; };
clippy = { clippy = {
amd64 = (compile { inherit git_version; compiler = "clippy"; }).workspace.garage { compileMode = "build"; } ; amd64 = (compile { compiler = "clippy"; }).workspace.garage { compileMode = "build"; } ;
}; };
} }
+8
View File
@@ -0,0 +1,8 @@
+++
title = "Benchmarks"
weight = 60
sort_by = "weight"
template = "documentation.html"
+++
Hello
+17
View File
@@ -0,0 +1,17 @@
+++
title = "Abstraction cost"
weight = 30
+++
We take as our baseline the raw disk sequential write performance.
We then compare Garage's performances to it, the difference represents what we call our "abstraction cost".
fsync, chunking, compression, pipelining, synchronization
# raw perf VS garage
# garage tmpfs VS garage std
# garage various multipart sizes
# garage 0.7.2.1 VS upstream
+18
View File
@@ -0,0 +1,18 @@
+++
title = "Failure & recovery"
weight = 50
+++
# Failure impact
Failures will lead to timeouts, which in turn could
lead to failed requests (this is a bug if failure enters in Garage tolerance)
and to increased latency as some retries might be performed.
How we proceed: we pause (`kill -STOP xxx`) one Garage process.
The idea is we don't want to close the TCP connection that would signal too easily
that a crash occured. Instead, we want to simulate a network error
or an overloaded process, ie. a 'non-collaborating' crash.
# Recovery impact
+14
View File
@@ -0,0 +1,14 @@
+++
title = "Industry tools"
weight = 60
+++
# minio warp
# intel-cloud cosbench
# (non retenu)
- wasabi s3-benchmark
- https://github.com/dvassallo/s3-benchmark
+10
View File
@@ -0,0 +1,10 @@
+++
title = "Liveness"
weight = 40
+++
freedom from starvation, backpressure, etc.
# Responsiveness under read/write load
@@ -0,0 +1,14 @@
+++
title = "Network sensitiveness"
weight = 10
+++
impact of node count and their latency
# Latency amplification
# Cluster size impact
# Time-To-First-Byte (TTFB)
with various object size
+16
View File
@@ -0,0 +1,16 @@
+++
title = "Pushing limits"
weight = 60
+++
# Many objects
# Huge objects
# Many nodes (horizontal scalability)
# Large nodes (vertical scalability)
+8
View File
@@ -0,0 +1,8 @@
+++
title = "Real world"
weight = 80
+++
# Nextcloud
# Peertube
+16 -33
View File
@@ -20,24 +20,6 @@ sudo apt-get update
sudo apt-get install build-essential 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` ## Using source from `crates.io`
Garage's source code is published on `crates.io`, Rust's official package repository. Garage's source code is published on `crates.io`, Rust's official package repository.
@@ -57,20 +39,21 @@ sudo cp $HOME/.cargo/bin/garage /usr/local/bin/garage
``` ```
## Selecting features to activate in your build ## Using source from the Gitea repository
Garage supports a number of compilation options in the form of Cargo features, The primary location for Garage's source code is the
which can be used to provide builds adapted to your system and your use case. [Gitea repository](https://git.deuxfleurs.fr/Deuxfleurs/garage).
The following features are available:
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`.
| 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 |
-22
View File
@@ -280,25 +280,3 @@ Traefik's caching middleware is only available on [entreprise version](https://d
[http.middlewares] [http.middlewares]
[http.middlewares.gzip_compress.compress] [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.
+1 -1
View File
@@ -1,6 +1,6 @@
+++ +++
title = "Development" title = "Development"
weight = 6 weight = 70
sort_by = "weight" sort_by = "weight"
template = "documentation.html" template = "documentation.html"
+++ +++
+1 -1
View File
@@ -1,6 +1,6 @@
+++ +++
title = "Working Documents" title = "Working Documents"
weight = 7 weight = 80
sort_by = "weight" sort_by = "weight"
template = "documentation.html" template = "documentation.html"
+++ +++
+4 -4
View File
@@ -8,10 +8,10 @@ rec {
sha256 = "1xy9zpypqfxs5gcq5dcla4bfkhxmh5nzn9dyqkr03lqycm9wg5cr"; sha256 = "1xy9zpypqfxs5gcq5dcla4bfkhxmh5nzn9dyqkr03lqycm9wg5cr";
}; };
cargo2nixSrc = fetchGit { cargo2nixSrc = fetchGit {
# As of 2022-08-29, stacking two patches: superboum@dedup_propagate and Alexis211@fix_fetchcrategit # As of 2022-03-17
url = "https://github.com/Alexis211/cargo2nix"; url = "https://github.com/superboum/cargo2nix";
ref = "fix_fetchcrategit"; ref = "dedup_propagate";
rev = "4b31c0cc05b6394916d46e9289f51263d81973b9"; rev = "486675c67249e735dd7eb68e1b9feac9db102be7";
}; };
/* /*
+13 -30
View File
@@ -117,13 +117,18 @@ 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.). 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. 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. 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 { (pkgs.rustBuilder.rustLib.makeOverride {
name = "garage"; name = "garage";
overrideAttrs = drv: {
/* [1] */ setBuildEnv = (buildEnv drv);
/* [2] */ hardeningDisable = [ "pie" ];
};
})
(pkgs.rustBuilder.rustLib.makeOverride {
name = "garage_rpc";
overrideAttrs = drv: overrideAttrs = drv:
(if git_version != null then { (if git_version != null then {
/* [3] */ preConfigure = '' /* [3] */ preConfigure = ''
@@ -131,22 +136,14 @@ let
export GIT_VERSION="${git_version}" export GIT_VERSION="${git_version}"
''; '';
} else {}) } else {})
// // {
{ /* [1] */ setBuildEnv = (buildEnv drv);
/* [1] */ setBuildEnv = (buildEnv drv); };
/* [2] */ hardeningDisable = [ "pie" ];
};
overrideArgs = old: { overrideArgs = old: {
/* [4] */ features = [ "bundled-libs" "sled" ] /* [4] */ features = if release then [ "kubernetes-discovery" ] else [];
++ (if release then [ "kubernetes-discovery" "telemetry-otlp" "metrics" "lmdb" "sqlite" ] else []);
}; };
}) })
(pkgs.rustBuilder.rustLib.makeOverride {
name = "garage_rpc";
overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); };
})
(pkgs.rustBuilder.rustLib.makeOverride { (pkgs.rustBuilder.rustLib.makeOverride {
name = "garage_db"; name = "garage_db";
overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); };
@@ -186,20 +183,6 @@ let
name = "k2v-client"; name = "k2v-client";
overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; 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; packageFun = import ../Cargo.nix;
+1 -1
View File
@@ -11,7 +11,7 @@ PATH="${GARAGE_DEBUG}:${GARAGE_RELEASE}:${NIX_RELEASE}:$PATH"
FANCYCOLORS=("41m" "42m" "44m" "45m" "100m" "104m") FANCYCOLORS=("41m" "42m" "44m" "45m" "100m" "104m")
export RUST_BACKTRACE=1 export RUST_BACKTRACE=1
export RUST_LOG=garage=info,garage_api=debug,netapp=trace export RUST_LOG=garage=info,garage_api=debug
MAIN_LABEL="\e[${FANCYCOLORS[0]}[main]\e[49m" MAIN_LABEL="\e[${FANCYCOLORS[0]}[main]\e[49m"
WHICH_GARAGE=$(which garage || exit 1) WHICH_GARAGE=$(which garage || exit 1)
+13 -13
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_api" name = "garage_api"
version = "0.8.0" version = "0.7.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
@@ -14,25 +14,25 @@ path = "lib.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
garage_model = { version = "0.8.0", path = "../model" } garage_model = { version = "0.7.0", path = "../model" }
garage_table = { version = "0.8.0", path = "../table" } garage_table = { version = "0.7.0", path = "../table" }
garage_block = { version = "0.8.0", path = "../block" } garage_block = { version = "0.7.0", path = "../block" }
garage_util = { version = "0.8.0", path = "../util" } garage_util = { version = "0.7.0", path = "../util" }
garage_rpc = { version = "0.8.0", path = "../rpc" } garage_rpc = { version = "0.7.0", path = "../rpc" }
async-trait = "0.1.7" async-trait = "0.1.7"
base64 = "0.13" base64 = "0.13"
bytes = "1.0" bytes = "1.0"
chrono = "0.4" chrono = "0.4"
crypto-common = "0.1" crypto-mac = "0.10"
err-derive = "0.3" err-derive = "0.3"
hex = "0.4" hex = "0.4"
hmac = "0.12" hmac = "0.10"
idna = "0.2" idna = "0.2"
tracing = "0.1.30" tracing = "0.1.30"
md-5 = "0.10" md-5 = "0.9"
nom = "7.1" nom = "7.1"
sha2 = "0.10" sha2 = "0.9"
futures = "0.3" futures = "0.3"
futures-util = "0.3" futures-util = "0.3"
@@ -54,9 +54,9 @@ quick-xml = { version = "0.21", features = [ "serialize" ] }
url = "2.1" url = "2.1"
opentelemetry = "0.17" opentelemetry = "0.17"
opentelemetry-prometheus = { version = "0.10", optional = true } opentelemetry-prometheus = "0.10"
prometheus = { version = "0.13", optional = true } opentelemetry-otlp = "0.10"
prometheus = "0.13"
[features] [features]
k2v = [ "garage_util/k2v", "garage_model/k2v" ] k2v = [ "garage_util/k2v", "garage_model/k2v" ]
metrics = [ "opentelemetry-prometheus", "prometheus" ]
+28 -40
View File
@@ -1,17 +1,15 @@
use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use futures::future::Future; use futures::future::Future;
use http::header::{ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW}; use http::header::{
ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW, CONTENT_TYPE,
};
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use opentelemetry::trace::SpanRef; use opentelemetry::trace::{SpanRef, Tracer};
#[cfg(feature = "metrics")]
use opentelemetry_prometheus::PrometheusExporter; use opentelemetry_prometheus::PrometheusExporter;
#[cfg(feature = "metrics")]
use prometheus::{Encoder, TextEncoder}; use prometheus::{Encoder, TextEncoder};
use garage_model::garage::Garage; use garage_model::garage::Garage;
@@ -27,7 +25,6 @@ use crate::admin::router::{Authorization, Endpoint};
pub struct AdminApiServer { pub struct AdminApiServer {
garage: Arc<Garage>, garage: Arc<Garage>,
#[cfg(feature = "metrics")]
exporter: PrometheusExporter, exporter: PrometheusExporter,
metrics_token: Option<String>, metrics_token: Option<String>,
admin_token: Option<String>, admin_token: Option<String>,
@@ -35,6 +32,7 @@ pub struct AdminApiServer {
impl AdminApiServer { impl AdminApiServer {
pub fn new(garage: Arc<Garage>) -> Self { pub fn new(garage: Arc<Garage>) -> Self {
let exporter = opentelemetry_prometheus::exporter().init();
let cfg = &garage.config.admin; let cfg = &garage.config.admin;
let metrics_token = cfg let metrics_token = cfg
.metrics_token .metrics_token
@@ -46,22 +44,21 @@ impl AdminApiServer {
.map(|tok| format!("Bearer {}", tok)); .map(|tok| format!("Bearer {}", tok));
Self { Self {
garage, garage,
#[cfg(feature = "metrics")] exporter,
exporter: opentelemetry_prometheus::exporter().init(),
metrics_token, metrics_token,
admin_token, admin_token,
} }
} }
pub async fn run( pub async fn run(self, shutdown_signal: impl Future<Output = ()>) -> Result<(), GarageError> {
self, if let Some(bind_addr) = self.garage.config.admin.api_bind_addr {
bind_addr: SocketAddr, let region = self.garage.config.s3_api.s3_region.clone();
shutdown_signal: impl Future<Output = ()>, ApiServer::new(region, self)
) -> Result<(), GarageError> { .run_server(bind_addr, shutdown_signal)
let region = self.garage.config.s3_api.s3_region.clone(); .await
ApiServer::new(region, self) } else {
.run_server(bind_addr, shutdown_signal) Ok(())
.await }
} }
fn handle_options(&self, _req: &Request<Body>) -> Result<Response<Body>, Error> { fn handle_options(&self, _req: &Request<Body>) -> Result<Response<Body>, Error> {
@@ -74,31 +71,22 @@ impl AdminApiServer {
} }
fn handle_metrics(&self) -> Result<Response<Body>, Error> { fn handle_metrics(&self) -> Result<Response<Body>, Error> {
#[cfg(feature = "metrics")] let mut buffer = vec![];
{ let encoder = TextEncoder::new();
use opentelemetry::trace::Tracer;
let mut buffer = vec![]; let tracer = opentelemetry::global::tracer("garage");
let encoder = TextEncoder::new(); let metric_families = tracer.in_span("admin/gather_metrics", |_| {
self.exporter.registry().gather()
});
let tracer = opentelemetry::global::tracer("garage"); encoder
let metric_families = tracer.in_span("admin/gather_metrics", |_| { .encode(&metric_families, &mut buffer)
self.exporter.registry().gather() .ok_or_internal_error("Could not serialize metrics")?;
});
encoder Ok(Response::builder()
.encode(&metric_families, &mut buffer) .status(200)
.ok_or_internal_error("Could not serialize metrics")?; .header(CONTENT_TYPE, encoder.format_type())
.body(Body::from(buffer))?)
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(),
))
} }
} }
+1 -3
View File
@@ -18,8 +18,7 @@ use crate::helpers::{json_ok_response, parse_json_body};
pub async fn handle_get_cluster_status(garage: &Arc<Garage>) -> Result<Response<Body>, Error> { pub async fn handle_get_cluster_status(garage: &Arc<Garage>) -> Result<Response<Body>, Error> {
let res = GetClusterStatusResponse { let res = GetClusterStatusResponse {
node: hex::encode(garage.system.id), node: hex::encode(garage.system.id),
garage_version: garage_util::version::garage_version(), garage_version: garage.system.garage_version(),
garage_features: garage_util::version::garage_features(),
db_engine: garage.db.engine(), db_engine: garage.db.engine(),
known_nodes: garage known_nodes: garage
.system .system
@@ -100,7 +99,6 @@ fn get_cluster_layout(garage: &Arc<Garage>) -> GetClusterLayoutResponse {
struct GetClusterStatusResponse { struct GetClusterStatusResponse {
node: String, node: String,
garage_version: &'static str, garage_version: &'static str,
garage_features: Option<&'static [&'static str]>,
db_engine: String, db_engine: String,
known_nodes: HashMap<String, KnownNodeResp>, known_nodes: HashMap<String, KnownNodeResp>,
layout: GetClusterLayoutResponse, layout: GetClusterLayoutResponse,
+10 -4
View File
@@ -1,4 +1,3 @@
use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
@@ -37,13 +36,20 @@ pub(crate) struct K2VApiEndpoint {
impl K2VApiServer { impl K2VApiServer {
pub async fn run( pub async fn run(
garage: Arc<Garage>, garage: Arc<Garage>,
bind_addr: SocketAddr,
s3_region: String,
shutdown_signal: impl Future<Output = ()>, shutdown_signal: impl Future<Output = ()>,
) -> Result<(), GarageError> { ) -> Result<(), GarageError> {
ApiServer::new(s3_region, K2VApiServer { garage }) 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) .run_server(bind_addr, shutdown_signal)
.await .await
} else {
Ok(())
}
} }
} }
+8 -6
View File
@@ -1,4 +1,3 @@
use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
@@ -44,13 +43,16 @@ pub(crate) struct S3ApiEndpoint {
impl S3ApiServer { impl S3ApiServer {
pub async fn run( pub async fn run(
garage: Arc<Garage>, garage: Arc<Garage>,
addr: SocketAddr,
s3_region: String,
shutdown_signal: impl Future<Output = ()>, shutdown_signal: impl Future<Output = ()>,
) -> Result<(), GarageError> { ) -> Result<(), GarageError> {
ApiServer::new(s3_region, S3ApiServer { garage }) let addr = garage.config.s3_api.api_bind_addr;
.run_server(addr, shutdown_signal)
.await ApiServer::new(
garage.config.s3_api.s3_region.clone(),
S3ApiServer { garage },
)
.run_server(addr, shutdown_signal)
.await
} }
async fn handle_request_without_bucket( async fn handle_request_without_bucket(
+1
View File
@@ -295,6 +295,7 @@ fn parse_create_bucket_xml(xml_bytes: &[u8]) -> Option<Option<String>> {
let mut ret = None; let mut ret = None;
for item in cbc.children() { for item in cbc.children() {
println!("{:?}", item);
if item.has_tag_name("LocationConstraint") { if item.has_tag_name("LocationConstraint") {
if ret != None { if ret != None {
return None; return None;
+11 -18
View File
@@ -5,12 +5,9 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
use futures::{stream, stream::Stream, StreamExt, TryFutureExt}; use futures::{stream, stream::Stream, StreamExt, TryFutureExt};
use md5::{Digest as Md5Digest, Md5}; use md5::{Digest as Md5Digest, Md5};
use bytes::Bytes;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use serde::Serialize; use serde::Serialize;
use garage_rpc::netapp::bytes_buf::BytesBuf;
use garage_rpc::rpc_helper::OrderTag;
use garage_table::*; use garage_table::*;
use garage_util::data::*; use garage_util::data::*;
use garage_util::time::*; use garage_util::time::*;
@@ -308,18 +305,13 @@ pub async fn handle_upload_part_copy(
// if and only if the block returned is a block that already existed // 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). // in the Garage data store (thus we don't need to save it again).
let garage2 = garage.clone(); let garage2 = garage.clone();
let order_stream = OrderTag::stream();
let source_blocks = stream::iter(blocks_to_copy) let source_blocks = stream::iter(blocks_to_copy)
.enumerate() .flat_map(|(block_hash, range_to_copy)| {
.flat_map(|(i, (block_hash, range_to_copy))| {
let garage3 = garage2.clone(); let garage3 = garage2.clone();
stream::once(async move { stream::once(async move {
let data = garage3 let data = garage3.block_manager.rpc_get_block(&block_hash).await?;
.block_manager
.rpc_get_block(&block_hash, Some(order_stream.order(i as u64)))
.await?;
match range_to_copy { match range_to_copy {
Some(r) => Ok((data.slice(r), None)), Some(r) => Ok((data[r].to_vec(), None)),
None => Ok((data, Some(block_hash))), None => Ok((data, Some(block_hash))),
} }
}) })
@@ -561,13 +553,13 @@ impl CopyPreconditionHeaders {
} }
} }
type BlockStreamItemOk = (Bytes, Option<Hash>); type BlockStreamItemOk = (Vec<u8>, Option<Hash>);
type BlockStreamItem = Result<BlockStreamItemOk, garage_util::error::Error>; type BlockStreamItem = Result<BlockStreamItemOk, garage_util::error::Error>;
struct Defragmenter<S: Stream<Item = BlockStreamItem>> { struct Defragmenter<S: Stream<Item = BlockStreamItem>> {
block_size: usize, block_size: usize,
block_stream: Pin<Box<stream::Peekable<S>>>, block_stream: Pin<Box<stream::Peekable<S>>>,
buffer: BytesBuf, buffer: Vec<u8>,
hash: Option<Hash>, hash: Option<Hash>,
} }
@@ -576,7 +568,7 @@ impl<S: Stream<Item = BlockStreamItem>> Defragmenter<S> {
Self { Self {
block_size, block_size,
block_stream, block_stream,
buffer: BytesBuf::new(), buffer: vec![],
hash: None, hash: None,
} }
} }
@@ -594,7 +586,7 @@ impl<S: Stream<Item = BlockStreamItem>> Defragmenter<S> {
if self.buffer.is_empty() { if self.buffer.is_empty() {
let (next_block, next_block_hash) = self.block_stream.next().await.unwrap()?; let (next_block, next_block_hash) = self.block_stream.next().await.unwrap()?;
self.buffer.extend(next_block); self.buffer = next_block;
self.hash = next_block_hash; self.hash = next_block_hash;
} else if self.buffer.len() + peeked_next_block.len() > self.block_size { } else if self.buffer.len() + peeked_next_block.len() > self.block_size {
break; break;
@@ -605,11 +597,11 @@ impl<S: Stream<Item = BlockStreamItem>> Defragmenter<S> {
} }
} }
Ok((self.buffer.take_all(), self.hash.take())) Ok((std::mem::take(&mut self.buffer), self.hash.take()))
} }
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct CopyObjectResult { pub struct CopyObjectResult {
#[serde(rename = "LastModified")] #[serde(rename = "LastModified")]
pub last_modified: s3_xml::Value, pub last_modified: s3_xml::Value,
@@ -617,7 +609,7 @@ pub struct CopyObjectResult {
pub etag: s3_xml::Value, pub etag: s3_xml::Value,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct CopyPartResult { pub struct CopyPartResult {
#[serde(serialize_with = "xmlns_tag")] #[serde(serialize_with = "xmlns_tag")]
pub xmlns: (), pub xmlns: (),
@@ -662,6 +654,7 @@ mod tests {
last_modified: s3_xml::Value("2011-04-11T20:34:56.000Z".into()), last_modified: s3_xml::Value("2011-04-11T20:34:56.000Z".into()),
etag: s3_xml::Value("\"9b2cf535f27731c974343645a3985328\"".into()), etag: s3_xml::Value("\"9b2cf535f27731c974343645a3985328\"".into()),
}; };
println!("{}", to_xml_with_header(&v)?);
assert_eq!(to_xml_with_header(&v)?, expected_retval); assert_eq!(to_xml_with_header(&v)?, expected_retval);
+32 -78
View File
@@ -7,9 +7,9 @@ use http::header::{
ACCEPT_RANGES, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, ETAG, IF_MODIFIED_SINCE, ACCEPT_RANGES, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, ETAG, IF_MODIFIED_SINCE,
IF_NONE_MATCH, LAST_MODIFIED, RANGE, IF_NONE_MATCH, LAST_MODIFIED, RANGE,
}; };
use hyper::body::Bytes;
use hyper::{Body, Request, Response, StatusCode}; use hyper::{Body, Request, Response, StatusCode};
use garage_rpc::rpc_helper::{netapp::stream::ByteStream, OrderTag};
use garage_table::EmptyKey; use garage_table::EmptyKey;
use garage_util::data::*; use garage_util::data::*;
@@ -242,15 +242,10 @@ pub async fn handle_get(
Ok(resp_builder.body(body)?) Ok(resp_builder.body(body)?)
} }
ObjectVersionData::FirstBlock(_, first_block_hash) => { ObjectVersionData::FirstBlock(_, first_block_hash) => {
let order_stream = OrderTag::stream(); 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, Some(order_stream.order(0)));
let get_next_blocks = garage.version_table.get(&last_v.uuid, &EmptyKey); let get_next_blocks = garage.version_table.get(&last_v.uuid, &EmptyKey);
let (first_block_stream, version) = let (first_block, version) = futures::try_join!(read_first_block, get_next_blocks)?;
futures::try_join!(read_first_block, get_next_blocks)?;
let version = version.ok_or(Error::NoSuchKey)?; let version = version.ok_or(Error::NoSuchKey)?;
let mut blocks = version let mut blocks = version
@@ -259,26 +254,24 @@ pub async fn handle_get(
.iter() .iter()
.map(|(_, vb)| (vb.hash, None)) .map(|(_, vb)| (vb.hash, None))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
blocks[0].1 = Some(first_block_stream); blocks[0].1 = Some(first_block);
let body_stream = futures::stream::iter(blocks) let body_stream = futures::stream::iter(blocks)
.enumerate() .map(move |(hash, data_opt)| {
.map(move |(i, (hash, stream_opt))| {
let garage = garage.clone(); let garage = garage.clone();
async move { async move {
if let Some(stream) = stream_opt { if let Some(data) = data_opt {
stream Ok(Bytes::from(data))
} else { } else {
garage garage
.block_manager .block_manager
.rpc_get_block_streaming(&hash, Some(order_stream.order(i as u64))) .rpc_get_block(&hash)
.await .await
.unwrap_or_else(|e| error_stream(i, e)) .map(Bytes::from)
} }
} }
}) })
.buffered(2) .buffered(2);
.flatten();
let body = hyper::body::Body::wrap_stream(body_stream); let body = hyper::body::Body::wrap_stream(body_stream);
Ok(resp_builder.body(body)?) Ok(resp_builder.body(body)?)
@@ -429,79 +422,40 @@ fn body_from_blocks_range(
all_blocks.len(), all_blocks.len(),
4 + ((end - begin) / std::cmp::max(all_blocks[0].1.size as u64, 1024)) as usize, 4 + ((end - begin) / std::cmp::max(all_blocks[0].1.size as u64, 1024)) as usize,
)); ));
let mut block_offset: u64 = 0; let mut true_offset = 0;
for (_, b) in all_blocks.iter() { for (_, b) in all_blocks.iter() {
if block_offset >= end { if true_offset >= end {
break; break;
} }
// Keep only blocks that have an intersection with the requested range // Keep only blocks that have an intersection with the requested range
if block_offset < end && block_offset + b.size > begin { if true_offset < end && true_offset + b.size > begin {
blocks.push((*b, block_offset)); blocks.push((*b, true_offset));
} }
block_offset += b.size as u64; true_offset += b.size;
} }
let order_stream = OrderTag::stream();
let body_stream = futures::stream::iter(blocks) let body_stream = futures::stream::iter(blocks)
.enumerate() .map(move |(block, true_offset)| {
.map(move |(i, (block, block_offset))| {
let garage = garage.clone(); let garage = garage.clone();
async move { async move {
garage let data = garage.block_manager.rpc_get_block(&block.hash).await?;
.block_manager let data = Bytes::from(data);
.rpc_get_block_streaming(&block.hash, Some(order_stream.order(i as u64))) let start_in_block = if true_offset > begin {
.await 0
.unwrap_or_else(|e| error_stream(i, e)) } else {
.scan(block_offset, move |chunk_offset, chunk| { begin - true_offset
let r = match chunk { };
Ok(chunk_bytes) => { let end_in_block = if true_offset + block.size < end {
let chunk_len = chunk_bytes.len() as u64; block.size
let r = if *chunk_offset >= end { } else {
// The current chunk is after the part we want to read. end - true_offset
// Returning None here will stop the scan, the rest of the };
// stream will be ignored Result::<Bytes, Error>::Ok(
None data.slice(start_in_block as usize..end_in_block as usize),
} 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) 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),
))
}))
}
+22 -42
View File
@@ -1,4 +1,4 @@
use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
use std::sync::Arc; use std::sync::Arc;
use futures::prelude::*; use futures::prelude::*;
@@ -8,14 +8,7 @@ use hyper::{Request, Response};
use md5::{digest::generic_array::*, Digest as Md5Digest, Md5}; use md5::{digest::generic_array::*, Digest as Md5Digest, Md5};
use sha2::Sha256; use sha2::Sha256;
use opentelemetry::{
trace::{FutureExt as OtelFutureExt, TraceContextExt, Tracer},
Context,
};
use garage_rpc::netapp::bytes_buf::BytesBuf;
use garage_table::*; use garage_table::*;
use garage_util::async_hash::*;
use garage_util::data::*; use garage_util::data::*;
use garage_util::error::Error as GarageError; use garage_util::error::Error as GarageError;
use garage_util::time::*; use garage_util::time::*;
@@ -109,7 +102,7 @@ pub(crate) async fn save_stream<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
size, size,
etag: data_md5sum_hex.clone(), etag: data_md5sum_hex.clone(),
}, },
first_block.to_vec(), first_block,
)), )),
}; };
@@ -137,7 +130,7 @@ pub(crate) async fn save_stream<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
garage.version_table.insert(&version).await?; garage.version_table.insert(&version).await?;
// Transfer data and verify checksum // Transfer data and verify checksum
let first_block_hash = async_blake2sum(first_block.clone()).await; let first_block_hash = blake2sum(&first_block[..]);
let tx_result = (|| async { let tx_result = (|| async {
let (total_size, data_md5sum, data_sha256sum) = read_and_put_blocks( let (total_size, data_md5sum, data_sha256sum) = read_and_put_blocks(
@@ -280,23 +273,14 @@ async fn read_and_put_blocks<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
garage: &Garage, garage: &Garage,
version: &Version, version: &Version,
part_number: u64, part_number: u64,
first_block: Bytes, first_block: Vec<u8>,
first_block_hash: Hash, first_block_hash: Hash,
chunker: &mut StreamChunker<S>, chunker: &mut StreamChunker<S>,
) -> Result<(u64, GenericArray<u8, typenum::U16>, Hash), Error> { ) -> Result<(u64, GenericArray<u8, typenum::U16>, Hash), Error> {
let tracer = opentelemetry::global::tracer("garage"); let mut md5hasher = Md5::new();
let mut sha256hasher = Sha256::new();
let md5hasher = AsyncHasher::<Md5>::new(); md5hasher.update(&first_block[..]);
let sha256hasher = AsyncHasher::<Sha256>::new(); sha256hasher.update(&first_block[..]);
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 next_offset = first_block.len();
let mut put_curr_version_block = put_block_meta( let mut put_curr_version_block = put_block_meta(
@@ -318,15 +302,9 @@ async fn read_and_put_blocks<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
chunker.next(), chunker.next(),
)?; )?;
if let Some(block) = next_block { if let Some(block) = next_block {
let (_, _, block_hash) = futures::future::join3( md5hasher.update(&block[..]);
md5hasher.update(block.clone()), sha256hasher.update(&block[..]);
sha256hasher.update(block.clone()), let block_hash = blake2sum(&block[..]);
async_blake2sum(block.clone()),
)
.with_context(Context::current_with_span(
tracer.start("Hash block (md5, sha256, blake2)"),
))
.await;
let block_len = block.len(); let block_len = block.len();
put_curr_version_block = put_block_meta( put_curr_version_block = put_block_meta(
garage, garage,
@@ -344,9 +322,9 @@ async fn read_and_put_blocks<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
} }
let total_size = next_offset as u64; let total_size = next_offset as u64;
let data_md5sum = md5hasher.finalize().await; let data_md5sum = md5hasher.finalize();
let data_sha256sum = sha256hasher.finalize().await; let data_sha256sum = sha256hasher.finalize();
let data_sha256sum = Hash::try_from(&data_sha256sum[..]).unwrap(); let data_sha256sum = Hash::try_from(&data_sha256sum[..]).unwrap();
Ok((total_size, data_md5sum, data_sha256sum)) Ok((total_size, data_md5sum, data_sha256sum))
@@ -386,7 +364,7 @@ struct StreamChunker<S: Stream<Item = Result<Bytes, Error>>> {
stream: S, stream: S,
read_all: bool, read_all: bool,
block_size: usize, block_size: usize,
buf: BytesBuf, buf: VecDeque<u8>,
} }
impl<S: Stream<Item = Result<Bytes, Error>> + Unpin> StreamChunker<S> { impl<S: Stream<Item = Result<Bytes, Error>> + Unpin> StreamChunker<S> {
@@ -395,11 +373,11 @@ impl<S: Stream<Item = Result<Bytes, Error>> + Unpin> StreamChunker<S> {
stream, stream,
read_all: false, read_all: false,
block_size, block_size,
buf: BytesBuf::new(), buf: VecDeque::with_capacity(2 * block_size),
} }
} }
async fn next(&mut self) -> Result<Option<Bytes>, Error> { async fn next(&mut self) -> Result<Option<Vec<u8>>, 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 { if let Some(block) = self.stream.next().await {
let bytes = block?; let bytes = block?;
@@ -412,8 +390,12 @@ impl<S: Stream<Item = Result<Bytes, Error>> + Unpin> StreamChunker<S> {
if self.buf.is_empty() { if self.buf.is_empty() {
Ok(None) Ok(None)
} else if self.buf.len() <= self.block_size {
let block = self.buf.drain(..).collect::<Vec<u8>>();
Ok(Some(block))
} else { } else {
Ok(Some(self.buf.take_max(self.block_size))) let block = self.buf.drain(..self.block_size).collect::<Vec<u8>>();
Ok(Some(block))
} }
} }
} }
@@ -522,9 +504,7 @@ pub async fn handle_put_part(
// Copy block to store // Copy block to store
let version = Version::new(version_uuid, bucket_id, key, false); let version = Version::new(version_uuid, bucket_id, key, false);
let first_block_hash = blake2sum(&first_block[..]);
let first_block_hash = async_blake2sum(first_block.clone()).await;
let (_, data_md5sum, data_sha256sum) = read_and_put_blocks( let (_, data_md5sum, data_sha256sum) = read_and_put_blocks(
&garage, &garage,
&version, &version,
+21 -21
View File
@@ -25,7 +25,7 @@ impl From<&str> for Value {
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct IntValue(#[serde(rename = "$value")] pub i64); pub struct IntValue(#[serde(rename = "$value")] pub i64);
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct Bucket { pub struct Bucket {
#[serde(rename = "CreationDate")] #[serde(rename = "CreationDate")]
pub creation_date: Value, pub creation_date: Value,
@@ -33,7 +33,7 @@ pub struct Bucket {
pub name: Value, pub name: Value,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct Owner { pub struct Owner {
#[serde(rename = "DisplayName")] #[serde(rename = "DisplayName")]
pub display_name: Value, pub display_name: Value,
@@ -41,13 +41,13 @@ pub struct Owner {
pub id: Value, pub id: Value,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct BucketList { pub struct BucketList {
#[serde(rename = "Bucket")] #[serde(rename = "Bucket")]
pub entries: Vec<Bucket>, pub entries: Vec<Bucket>,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct ListAllMyBucketsResult { pub struct ListAllMyBucketsResult {
#[serde(rename = "Buckets")] #[serde(rename = "Buckets")]
pub buckets: BucketList, pub buckets: BucketList,
@@ -55,7 +55,7 @@ pub struct ListAllMyBucketsResult {
pub owner: Owner, pub owner: Owner,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct LocationConstraint { pub struct LocationConstraint {
#[serde(serialize_with = "xmlns_tag")] #[serde(serialize_with = "xmlns_tag")]
pub xmlns: (), pub xmlns: (),
@@ -63,7 +63,7 @@ pub struct LocationConstraint {
pub region: String, pub region: String,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct Deleted { pub struct Deleted {
#[serde(rename = "Key")] #[serde(rename = "Key")]
pub key: Value, pub key: Value,
@@ -73,7 +73,7 @@ pub struct Deleted {
pub delete_marker_version_id: Value, pub delete_marker_version_id: Value,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct Error { pub struct Error {
#[serde(rename = "Code")] #[serde(rename = "Code")]
pub code: Value, pub code: Value,
@@ -85,7 +85,7 @@ pub struct Error {
pub region: Option<Value>, pub region: Option<Value>,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct DeleteError { pub struct DeleteError {
#[serde(rename = "Code")] #[serde(rename = "Code")]
pub code: Value, pub code: Value,
@@ -97,7 +97,7 @@ pub struct DeleteError {
pub version_id: Option<Value>, pub version_id: Option<Value>,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct DeleteResult { pub struct DeleteResult {
#[serde(serialize_with = "xmlns_tag")] #[serde(serialize_with = "xmlns_tag")]
pub xmlns: (), pub xmlns: (),
@@ -107,7 +107,7 @@ pub struct DeleteResult {
pub errors: Vec<DeleteError>, pub errors: Vec<DeleteError>,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct InitiateMultipartUploadResult { pub struct InitiateMultipartUploadResult {
#[serde(serialize_with = "xmlns_tag")] #[serde(serialize_with = "xmlns_tag")]
pub xmlns: (), pub xmlns: (),
@@ -119,7 +119,7 @@ pub struct InitiateMultipartUploadResult {
pub upload_id: Value, pub upload_id: Value,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct CompleteMultipartUploadResult { pub struct CompleteMultipartUploadResult {
#[serde(serialize_with = "xmlns_tag")] #[serde(serialize_with = "xmlns_tag")]
pub xmlns: (), pub xmlns: (),
@@ -133,7 +133,7 @@ pub struct CompleteMultipartUploadResult {
pub etag: Value, pub etag: Value,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct Initiator { pub struct Initiator {
#[serde(rename = "DisplayName")] #[serde(rename = "DisplayName")]
pub display_name: Value, pub display_name: Value,
@@ -141,7 +141,7 @@ pub struct Initiator {
pub id: Value, pub id: Value,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct ListMultipartItem { pub struct ListMultipartItem {
#[serde(rename = "Initiated")] #[serde(rename = "Initiated")]
pub initiated: Value, pub initiated: Value,
@@ -157,7 +157,7 @@ pub struct ListMultipartItem {
pub storage_class: Value, pub storage_class: Value,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct ListMultipartUploadsResult { pub struct ListMultipartUploadsResult {
#[serde(serialize_with = "xmlns_tag")] #[serde(serialize_with = "xmlns_tag")]
pub xmlns: (), pub xmlns: (),
@@ -187,7 +187,7 @@ pub struct ListMultipartUploadsResult {
pub encoding_type: Option<Value>, pub encoding_type: Option<Value>,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct PartItem { pub struct PartItem {
#[serde(rename = "ETag")] #[serde(rename = "ETag")]
pub etag: Value, pub etag: Value,
@@ -199,7 +199,7 @@ pub struct PartItem {
pub size: IntValue, pub size: IntValue,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct ListPartsResult { pub struct ListPartsResult {
#[serde(serialize_with = "xmlns_tag")] #[serde(serialize_with = "xmlns_tag")]
pub xmlns: (), pub xmlns: (),
@@ -227,7 +227,7 @@ pub struct ListPartsResult {
pub storage_class: Value, pub storage_class: Value,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct ListBucketItem { pub struct ListBucketItem {
#[serde(rename = "Key")] #[serde(rename = "Key")]
pub key: Value, pub key: Value,
@@ -241,13 +241,13 @@ pub struct ListBucketItem {
pub storage_class: Value, pub storage_class: Value,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct CommonPrefix { pub struct CommonPrefix {
#[serde(rename = "Prefix")] #[serde(rename = "Prefix")]
pub prefix: Value, pub prefix: Value,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct ListBucketResult { pub struct ListBucketResult {
#[serde(serialize_with = "xmlns_tag")] #[serde(serialize_with = "xmlns_tag")]
pub xmlns: (), pub xmlns: (),
@@ -281,7 +281,7 @@ pub struct ListBucketResult {
pub common_prefixes: Vec<CommonPrefix>, pub common_prefixes: Vec<CommonPrefix>,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct VersioningConfiguration { pub struct VersioningConfiguration {
#[serde(serialize_with = "xmlns_tag")] #[serde(serialize_with = "xmlns_tag")]
pub xmlns: (), pub xmlns: (),
@@ -289,7 +289,7 @@ pub struct VersioningConfiguration {
pub status: Option<Value>, pub status: Option<Value>,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq)]
pub struct PostObject { pub struct PostObject {
#[serde(serialize_with = "xmlns_tag")] #[serde(serialize_with = "xmlns_tag")]
pub xmlns: (), pub xmlns: (),
+7 -7
View File
@@ -1,5 +1,5 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac, NewMac};
use sha2::Sha256; use sha2::Sha256;
use garage_util::data::{sha256sum, Hash}; use garage_util::data::{sha256sum, Hash};
@@ -29,17 +29,17 @@ pub fn signing_hmac(
secret_key: &str, secret_key: &str,
region: &str, region: &str,
service: &str, service: &str,
) -> Result<HmacSha256, crypto_common::InvalidLength> { ) -> Result<HmacSha256, crypto_mac::InvalidKeyLength> {
let secret = String::from("AWS4") + secret_key; let secret = String::from("AWS4") + secret_key;
let mut date_hmac = HmacSha256::new_from_slice(secret.as_bytes())?; let mut date_hmac = HmacSha256::new_varkey(secret.as_bytes())?;
date_hmac.update(datetime.format(SHORT_DATE).to_string().as_bytes()); date_hmac.update(datetime.format(SHORT_DATE).to_string().as_bytes());
let mut region_hmac = HmacSha256::new_from_slice(&date_hmac.finalize().into_bytes())?; let mut region_hmac = HmacSha256::new_varkey(&date_hmac.finalize().into_bytes())?;
region_hmac.update(region.as_bytes()); region_hmac.update(region.as_bytes());
let mut service_hmac = HmacSha256::new_from_slice(&region_hmac.finalize().into_bytes())?; let mut service_hmac = HmacSha256::new_varkey(&region_hmac.finalize().into_bytes())?;
service_hmac.update(service.as_bytes()); service_hmac.update(service.as_bytes());
let mut signing_hmac = HmacSha256::new_from_slice(&service_hmac.finalize().into_bytes())?; let mut signing_hmac = HmacSha256::new_varkey(&service_hmac.finalize().into_bytes())?;
signing_hmac.update(b"aws4_request"); signing_hmac.update(b"aws4_request");
let hmac = HmacSha256::new_from_slice(&signing_hmac.finalize().into_bytes())?; let hmac = HmacSha256::new_varkey(&signing_hmac.finalize().into_bytes())?;
Ok(hmac) Ok(hmac)
} }
+4 -10
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_block" name = "garage_block"
version = "0.8.0" version = "0.7.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
@@ -15,9 +15,9 @@ path = "lib.rs"
[dependencies] [dependencies]
garage_db = { version = "0.8.0", path = "../db" } garage_db = { version = "0.8.0", path = "../db" }
garage_rpc = { version = "0.8.0", path = "../rpc" } garage_rpc = { version = "0.7.0", path = "../rpc" }
garage_util = { version = "0.8.0", path = "../util" } garage_util = { version = "0.7.0", path = "../util" }
garage_table = { version = "0.8.0", path = "../table" } garage_table = { version = "0.7.0", path = "../table" }
opentelemetry = "0.17" opentelemetry = "0.17"
@@ -27,8 +27,6 @@ bytes = "1.0"
hex = "0.4" hex = "0.4"
tracing = "0.1.30" tracing = "0.1.30"
rand = "0.8" rand = "0.8"
async-compression = { version = "0.3", features = ["tokio", "zstd"] }
zstd = { version = "0.9", default-features = false } zstd = { version = "0.9", default-features = false }
rmp-serde = "0.15" rmp-serde = "0.15"
@@ -38,7 +36,3 @@ serde_bytes = "0.11"
futures = "0.3" futures = "0.3"
futures-util = "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 = { 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"] }
[features]
system-libs = [ "zstd/pkg-config" ]
+12 -36
View File
@@ -1,22 +1,16 @@
use bytes::Bytes;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use zstd::stream::{decode_all as zstd_decode, Encoder}; use zstd::stream::{decode_all as zstd_decode, Encoder};
use garage_util::data::*; use garage_util::data::*;
use garage_util::error::*; use garage_util::error::*;
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
pub enum DataBlockHeader {
Plain,
Compressed,
}
/// A possibly compressed block of data /// A possibly compressed block of data
#[derive(Debug, Serialize, Deserialize)]
pub enum DataBlock { pub enum DataBlock {
/// Uncompressed data /// Uncompressed data
Plain(Bytes), Plain(#[serde(with = "serde_bytes")] Vec<u8>),
/// Data compressed with zstd /// Data compressed with zstd
Compressed(Bytes), Compressed(#[serde(with = "serde_bytes")] Vec<u8>),
} }
impl DataBlock { impl DataBlock {
@@ -36,7 +30,7 @@ impl DataBlock {
/// Get the buffer, possibly decompressing it, and verify it's integrity. /// 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 /// For Plain block, data is compared to hash, for Compressed block, zstd checksumming system
/// is used instead. /// is used instead.
pub fn verify_get(self, hash: Hash) -> Result<Bytes, Error> { pub fn verify_get(self, hash: Hash) -> Result<Vec<u8>, Error> {
match self { match self {
DataBlock::Plain(data) => { DataBlock::Plain(data) => {
if blake2sum(&data) == hash { if blake2sum(&data) == hash {
@@ -45,9 +39,9 @@ impl DataBlock {
Err(Error::CorruptData(hash)) Err(Error::CorruptData(hash))
} }
} }
DataBlock::Compressed(data) => zstd_decode(&data[..]) DataBlock::Compressed(data) => {
.map_err(|_| Error::CorruptData(hash)) zstd_decode(&data[..]).map_err(|_| Error::CorruptData(hash))
.map(Bytes::from), }
} }
} }
@@ -67,31 +61,13 @@ impl DataBlock {
} }
} }
pub async fn from_buffer(data: Bytes, level: Option<i32>) -> DataBlock { pub fn from_buffer(data: Vec<u8>, level: Option<i32>) -> DataBlock {
tokio::task::spawn_blocking(move || { if let Some(level) = level {
if let Some(level) = level { if let Ok(data) = zstd_encode(&data[..], level) {
if let Ok(data) = zstd_encode(&data[..], level) { return DataBlock::Compressed(data);
return DataBlock::Compressed(data.into());
}
} }
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),
} }
DataBlock::Plain(data)
} }
} }
-1
View File
@@ -3,7 +3,6 @@ extern crate tracing;
pub mod manager; pub mod manager;
pub mod repair; pub mod repair;
pub mod resync;
mod block; mod block;
mod metrics; mod metrics;
+554 -271
View File
File diff suppressed because it is too large Load Diff
+6 -28
View File
@@ -19,17 +19,7 @@ use garage_util::tranquilizer::Tranquilizer;
use crate::manager::*; use crate::manager::*;
// Full scrub every 30 days const SCRUB_INTERVAL: Duration = Duration::from_secs(3600 * 24 * 30); // 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 { pub struct RepairWorker {
manager: Arc<BlockManager>, manager: Arc<BlockManager>,
@@ -112,9 +102,7 @@ impl Worker for RepairWorker {
} }
for hash in batch_of_hashes.into_iter() { for hash in batch_of_hashes.into_iter() {
self.manager self.manager.put_to_resync(&hash, Duration::from_secs(0))?;
.resync
.put_to_resync(&hash, Duration::from_secs(0))?;
self.next_start = Some(hash) self.next_start = Some(hash)
} }
@@ -126,9 +114,7 @@ impl Worker for RepairWorker {
// This allows us to find blocks we are storing but don't actually need, // 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. // so that we can offload them if necessary and then delete them locally.
if let Some(hash) = bi.next().await? { if let Some(hash) = bi.next().await? {
self.manager self.manager.put_to_resync(&hash, Duration::from_secs(0))?;
.resync
.put_to_resync(&hash, Duration::from_secs(0))?;
Ok(WorkerState::Busy) Ok(WorkerState::Busy)
} else { } else {
Ok(WorkerState::Done) Ok(WorkerState::Done)
@@ -142,13 +128,7 @@ 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 { pub struct ScrubWorker {
manager: Arc<BlockManager>, manager: Arc<BlockManager>,
@@ -196,7 +176,7 @@ impl ScrubWorker {
Ok(v) => v, Ok(v) => v,
Err(_) => ScrubWorkerPersisted { Err(_) => ScrubWorkerPersisted {
time_last_complete_scrub: 0, time_last_complete_scrub: 0,
tranquility: INITIAL_SCRUB_TRANQUILITY, tranquility: 4,
corruptions_detected: 0, corruptions_detected: 0,
}, },
}; };
@@ -363,9 +343,7 @@ impl Worker for ScrubWorker {
} }
} }
// ---- ---- ---- // ----
// UTILITY FOR ENUMERATING THE BLOCK STORE
// ---- ---- ----
struct BlockStoreIterator { struct BlockStoreIterator {
path: Vec<ReadingDir>, path: Vec<ReadingDir>,
-599
View File
@@ -1,599 +0,0 @@
use std::collections::HashSet;
use std::convert::TryInto;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use arc_swap::ArcSwap;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
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
// 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).
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;
pub struct BlockResyncManager {
pub(crate) queue: CountedTree,
pub(crate) notify: Notify,
pub(crate) errors: CountedTree,
busy_set: BusySet,
persister: Persister<ResyncPersistedConfig>,
persisted: ArcSwap<ResyncPersistedConfig>,
}
#[derive(Serialize, Deserialize, Clone, Copy)]
struct ResyncPersistedConfig {
n_workers: usize,
tranquility: u32,
}
enum ResyncIterResult {
BusyDidSomething,
BusyDidNothing,
IdleFor(Duration),
}
type BusySet = Arc<Mutex<HashSet<Vec<u8>>>>;
struct BusyBlock {
time_bytes: Vec<u8>,
hash_bytes: Vec<u8>,
busy_set: BusySet,
}
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 {
n_workers: 1,
tranquility: INITIAL_RESYNC_TRANQUILITY,
},
};
Self {
queue,
notify: Notify::new(),
errors,
busy_set: Arc::new(Mutex::new(HashSet::new())),
persister,
persisted: ArcSwap::new(Arc::new(persisted)),
}
}
/// Get lenght of resync queue
pub fn queue_len(&self) -> Result<usize, Error> {
// 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<usize, Error> {
// (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<ResyncIterResult, db::Error> {
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(&block.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(&block.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(&block.time_bytes)?;
} else {
self.errors.remove(hash.as_slice())?;
self.queue.remove(&block.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)))
}
}
fn get_block_to_resync(&self) -> Result<Option<BusyBlock>, 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(),
}));
}
}
Ok(None)
}
async fn resync_block(&self, manager: &BlockManager, hash: &Hash) -> Result<(), Error> {
let BlockStatus { exists, needed } = manager.check_block_status(hash).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 who_needs_resps = manager
.system
.rpc
.call_many(
&manager.endpoint,
&who,
BlockRpc::NeedBlockQuery(*hash),
RequestStrategy::with_priority(PRIO_BACKGROUND)
.with_timeout(NEED_BLOCK_QUERY_TIMEOUT),
)
.await?;
let mut need_nodes = vec![];
for (node, needed) in who_needs_resps {
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 block = manager.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);
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.delete_if_unneeded(hash).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, None).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_waiters();
Ok(())
}
pub async fn set_n_workers(&self, n_workers: usize) -> Result<(), Error> {
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
)));
}
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<BlockManager>,
tranquilizer: Tranquilizer,
next_delay: Duration,
}
impl ResyncWorker {
pub(crate) fn new(index: usize, manager: Arc<BlockManager>) -> Self {
Self {
index,
manager,
tranquilizer: Tranquilizer::new(30),
next_delay: Duration::from_secs(10),
}
}
}
#[async_trait]
impl Worker for ResyncWorker {
fn name(&self) -> String {
format!("Block resync worker #{}", self.index + 1)
}
fn info(&self) -> Option<String> {
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 = {}", persisted.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<bool>) -> Result<WorkerState, Error> {
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
.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<bool>) -> 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
}
}
/// 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<u8> {
[
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()
}
}
+3 -6
View File
@@ -21,9 +21,9 @@ err-derive = "0.3"
hexdump = "0.1" hexdump = "0.1"
tracing = "0.1.30" tracing = "0.1.30"
heed = { version = "0.11", default-features = false, features = ["lmdb"], optional = true } heed = "0.11"
rusqlite = { version = "0.27", optional = true } rusqlite = { version = "0.27", features = ["bundled"] }
sled = { version = "0.34", optional = true } sled = "0.34"
# cli deps # cli deps
clap = { version = "3.1.18", optional = true, features = ["derive", "env"] } clap = { version = "3.1.18", optional = true, features = ["derive", "env"] }
@@ -33,7 +33,4 @@ pretty_env_logger = { version = "0.4", optional = true }
mktemp = "0.4" mktemp = "0.4"
[features] [features]
bundled-libs = [ "rusqlite/bundled" ]
cli = ["clap", "pretty_env_logger"] cli = ["clap", "pretty_env_logger"]
lmdb = [ "heed" ]
sqlite = [ "rusqlite" ]
-7
View File
@@ -1,15 +1,8 @@
#[macro_use] #[macro_use]
#[cfg(feature = "sqlite")]
extern crate tracing; 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; pub mod lmdb_adapter;
#[cfg(feature = "sled")]
pub mod sled_adapter; pub mod sled_adapter;
#[cfg(feature = "sqlite")]
pub mod sqlite_adapter; pub mod sqlite_adapter;
pub mod counted_tree_hack; pub mod counted_tree_hack;
+18 -37
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage" name = "garage"
version = "0.8.0" version = "0.7.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
@@ -22,20 +22,20 @@ path = "tests/lib.rs"
[dependencies] [dependencies]
garage_db = { version = "0.8.0", path = "../db" } garage_db = { version = "0.8.0", path = "../db" }
garage_api = { version = "0.8.0", path = "../api" } garage_api = { version = "0.7.0", path = "../api" }
garage_block = { version = "0.8.0", path = "../block" } garage_block = { version = "0.7.0", path = "../block" }
garage_model = { version = "0.8.0", path = "../model" } garage_model = { version = "0.7.0", path = "../model" }
garage_rpc = { version = "0.8.0", path = "../rpc" } garage_rpc = { version = "0.7.0", path = "../rpc" }
garage_table = { version = "0.8.0", path = "../table" } garage_table = { version = "0.7.0", path = "../table" }
garage_util = { version = "0.8.0", path = "../util" } garage_util = { version = "0.7.0", path = "../util" }
garage_web = { version = "0.8.0", path = "../web" } garage_web = { version = "0.7.0", path = "../web" }
bytes = "1.0" bytes = "1.0"
bytesize = "1.1" bytesize = "1.1"
timeago = "0.3" timeago = "0.3"
hex = "0.4" hex = "0.4"
tracing = { version = "0.1.30", features = ["log-always"] } tracing = { version = "0.1.30", features = ["log-always"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] } pretty_env_logger = "0.4"
rand = "0.8" rand = "0.8"
async-trait = "0.1.7" async-trait = "0.1.7"
sodiumoxide = { version = "0.2.5-0", package = "kuska-sodiumoxide" } sodiumoxide = { version = "0.2.5-0", package = "kuska-sodiumoxide" }
@@ -50,20 +50,22 @@ futures = "0.3"
futures-util = "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 = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
netapp = "0.5" #netapp = { version = "0.3.2", git = "https://git.deuxfleurs.fr/lx/netapp" }
#netapp = { version = "0.4", path = "../../../netapp" }
netapp = "0.4"
opentelemetry = { version = "0.17", features = [ "rt-tokio" ] } opentelemetry = { version = "0.17", features = [ "rt-tokio" ] }
opentelemetry-prometheus = { version = "0.10", optional = true } opentelemetry-prometheus = "0.10"
opentelemetry-otlp = { version = "0.10", optional = true } opentelemetry-otlp = "0.10"
prometheus = { version = "0.13", optional = true } prometheus = "0.13"
[dev-dependencies] [dev-dependencies]
aws-sdk-s3 = "0.8" aws-sdk-s3 = "0.8"
chrono = "0.4" chrono = "0.4"
http = "0.2" http = "0.2"
hmac = "0.12" hmac = "0.10"
hyper = { version = "0.14", features = ["client", "http1", "runtime"] } hyper = { version = "0.14", features = ["client", "http1", "runtime"] }
sha2 = "0.10" sha2 = "0.9"
static_init = "1.0" static_init = "1.0"
assert-json-diff = "2.0" assert-json-diff = "2.0"
@@ -72,26 +74,5 @@ base64 = "0.13"
[features] [features]
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" ] kubernetes-discovery = [ "garage_rpc/kubernetes-discovery" ]
# Prometheus exporter (/metrics endpoint). k2v = [ "garage_util/k2v", "garage_api/k2v" ]
metrics = [ "garage_api/metrics", "opentelemetry-prometheus", "prometheus" ]
# Exporter for the OpenTelemetry Collector.
telemetry-otlp = [ "opentelemetry-otlp" ]
# 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" ]
+6 -37
View File
@@ -15,8 +15,6 @@ use garage_table::*;
use garage_rpc::*; use garage_rpc::*;
use garage_block::repair::ScrubWorkerCommand;
use garage_model::bucket_alias_table::*; use garage_model::bucket_alias_table::*;
use garage_model::bucket_table::*; use garage_model::bucket_table::*;
use garage_model::garage::Garage; use garage_model::garage::Garage;
@@ -683,7 +681,7 @@ impl AdminRpcHandler {
.endpoint .endpoint
.call( .call(
&node, &node,
AdminRpc::LaunchRepair(opt_to_send.clone()), &AdminRpc::LaunchRepair(opt_to_send.clone()),
PRIO_NORMAL, PRIO_NORMAL,
) )
.await; .await;
@@ -723,7 +721,7 @@ impl AdminRpcHandler {
let node_id = (*node).into(); let node_id = (*node).into();
match self match self
.endpoint .endpoint
.call(&node_id, AdminRpc::Stats(opt), PRIO_NORMAL) .call(&node_id, &AdminRpc::Stats(opt), PRIO_NORMAL)
.await? .await?
{ {
Ok(AdminRpc::Ok(s)) => writeln!(&mut ret, "{}", s).unwrap(), Ok(AdminRpc::Ok(s)) => writeln!(&mut ret, "{}", s).unwrap(),
@@ -741,11 +739,8 @@ impl AdminRpcHandler {
let mut ret = String::new(); let mut ret = String::new();
writeln!( writeln!(
&mut ret, &mut ret,
"\nGarage version: {} [features: {}]", "\nGarage version: {}",
garage_util::version::garage_version(), self.garage.system.garage_version(),
garage_util::version::garage_features()
.map(|list| list.join(", "))
.unwrap_or_else(|| "(unknown)".into()),
) )
.unwrap(); .unwrap();
writeln!(&mut ret, "\nDatabase engine: {}", self.garage.db.engine()).unwrap(); writeln!(&mut ret, "\nDatabase engine: {}", self.garage.db.engine()).unwrap();
@@ -784,13 +779,13 @@ impl AdminRpcHandler {
writeln!( writeln!(
&mut ret, &mut ret,
" resync queue length: {}", " resync queue length: {}",
self.garage.block_manager.resync.queue_len()? self.garage.block_manager.resync_queue_len()?
) )
.unwrap(); .unwrap();
writeln!( writeln!(
&mut ret, &mut ret,
" blocks with resync errors: {}", " blocks with resync errors: {}",
self.garage.block_manager.resync.errors_len()? self.garage.block_manager.resync_errors_len()?
) )
.unwrap(); .unwrap();
@@ -841,32 +836,6 @@ impl AdminRpcHandler {
let workers = self.garage.background.get_worker_info(); let workers = self.garage.background.get_worker_info();
Ok(AdminRpc::WorkerList(workers, opt)) 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::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
.resync
.set_tranquility(tranquility)
.await?;
Ok(AdminRpc::Ok("Resync tranquility updated".into()))
}
},
} }
} }
} }
+3 -3
View File
@@ -47,7 +47,7 @@ pub async fn cli_command_dispatch(
pub async fn cmd_status(rpc_cli: &Endpoint<SystemRpc, ()>, rpc_host: NodeID) -> Result<(), Error> { pub async fn cmd_status(rpc_cli: &Endpoint<SystemRpc, ()>, rpc_host: NodeID) -> Result<(), Error> {
let status = match rpc_cli let status = match rpc_cli
.call(&rpc_host, SystemRpc::GetKnownNodes, PRIO_NORMAL) .call(&rpc_host, &SystemRpc::GetKnownNodes, PRIO_NORMAL)
.await?? .await??
{ {
SystemRpc::ReturnKnownNodes(nodes) => nodes, SystemRpc::ReturnKnownNodes(nodes) => nodes,
@@ -149,7 +149,7 @@ pub async fn cmd_connect(
args: ConnectNodeOpt, args: ConnectNodeOpt,
) -> Result<(), Error> { ) -> Result<(), Error> {
match rpc_cli match rpc_cli
.call(&rpc_host, SystemRpc::Connect(args.node), PRIO_NORMAL) .call(&rpc_host, &SystemRpc::Connect(args.node), PRIO_NORMAL)
.await?? .await??
{ {
SystemRpc::Ok => { SystemRpc::Ok => {
@@ -165,7 +165,7 @@ pub async fn cmd_admin(
rpc_host: NodeID, rpc_host: NodeID,
args: AdminRpc, args: AdminRpc,
) -> Result<(), HelperError> { ) -> 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) => { AdminRpc::Ok(msg) => {
println!("{}", msg); println!("{}", msg);
} }
+3 -3
View File
@@ -36,7 +36,7 @@ pub async fn cmd_assign_role(
args: AssignRoleOpt, args: AssignRoleOpt,
) -> Result<(), Error> { ) -> Result<(), Error> {
let status = match rpc_cli let status = match rpc_cli
.call(&rpc_host, SystemRpc::GetKnownNodes, PRIO_NORMAL) .call(&rpc_host, &SystemRpc::GetKnownNodes, PRIO_NORMAL)
.await?? .await??
{ {
SystemRpc::ReturnKnownNodes(nodes) => nodes, SystemRpc::ReturnKnownNodes(nodes) => nodes,
@@ -245,7 +245,7 @@ pub async fn fetch_layout(
rpc_host: NodeID, rpc_host: NodeID,
) -> Result<ClusterLayout, Error> { ) -> Result<ClusterLayout, Error> {
match rpc_cli match rpc_cli
.call(&rpc_host, SystemRpc::PullClusterLayout, PRIO_NORMAL) .call(&rpc_host, &SystemRpc::PullClusterLayout, PRIO_NORMAL)
.await?? .await??
{ {
SystemRpc::AdvertiseClusterLayout(t) => Ok(t), SystemRpc::AdvertiseClusterLayout(t) => Ok(t),
@@ -261,7 +261,7 @@ pub async fn send_layout(
rpc_cli rpc_cli
.call( .call(
&rpc_host, &rpc_host,
SystemRpc::AdvertiseClusterLayout(layout), &SystemRpc::AdvertiseClusterLayout(layout),
PRIO_NORMAL, PRIO_NORMAL,
) )
.await??; .await??;
+51 -71
View File
@@ -1,65 +1,64 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use structopt::StructOpt;
use garage_util::version::garage_version; use structopt::StructOpt;
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
pub enum Command { pub enum Command {
/// Run Garage server /// Run Garage server
#[structopt(name = "server", version = garage_version())] #[structopt(name = "server")]
Server, Server,
/// Get network status /// Get network status
#[structopt(name = "status", version = garage_version())] #[structopt(name = "status")]
Status, Status,
/// Operations on individual Garage nodes /// Operations on individual Garage nodes
#[structopt(name = "node", version = garage_version())] #[structopt(name = "node")]
Node(NodeOperation), Node(NodeOperation),
/// Operations on the assignation of node roles in the cluster layout /// Operations on the assignation of node roles in the cluster layout
#[structopt(name = "layout", version = garage_version())] #[structopt(name = "layout")]
Layout(LayoutOperation), Layout(LayoutOperation),
/// Operations on buckets /// Operations on buckets
#[structopt(name = "bucket", version = garage_version())] #[structopt(name = "bucket")]
Bucket(BucketOperation), Bucket(BucketOperation),
/// Operations on S3 access keys /// Operations on S3 access keys
#[structopt(name = "key", version = garage_version())] #[structopt(name = "key")]
Key(KeyOperation), Key(KeyOperation),
/// Run migrations from previous Garage version /// Run migrations from previous Garage version
/// (DO NOT USE WITHOUT READING FULL DOCUMENTATION) /// (DO NOT USE WITHOUT READING FULL DOCUMENTATION)
#[structopt(name = "migrate", version = garage_version())] #[structopt(name = "migrate")]
Migrate(MigrateOpt), Migrate(MigrateOpt),
/// Start repair of node data on remote node /// Start repair of node data on remote node
#[structopt(name = "repair", version = garage_version())] #[structopt(name = "repair")]
Repair(RepairOpt), Repair(RepairOpt),
/// Offline reparation of node data (these repairs must be run offline /// Offline reparation of node data (these repairs must be run offline
/// directly on the server node) /// directly on the server node)
#[structopt(name = "offline-repair", version = garage_version())] #[structopt(name = "offline-repair")]
OfflineRepair(OfflineRepairOpt), OfflineRepair(OfflineRepairOpt),
/// Gather node statistics /// Gather node statistics
#[structopt(name = "stats", version = garage_version())] #[structopt(name = "stats")]
Stats(StatsOpt), Stats(StatsOpt),
/// Manage background workers /// Manage background workers
#[structopt(name = "worker", version = garage_version())] #[structopt(name = "worker")]
Worker(WorkerOpt), Worker(WorkerOpt),
} }
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
pub enum NodeOperation { pub enum NodeOperation {
/// Print identifier (public key) of this Garage node /// Print identifier (public key) of this Garage node
#[structopt(name = "id", version = garage_version())] #[structopt(name = "id")]
NodeId(NodeIdOpt), NodeId(NodeIdOpt),
/// Connect to Garage node that is currently isolated from the system /// Connect to Garage node that is currently isolated from the system
#[structopt(name = "connect", version = garage_version())] #[structopt(name = "connect")]
Connect(ConnectNodeOpt), Connect(ConnectNodeOpt),
} }
@@ -80,23 +79,23 @@ pub struct ConnectNodeOpt {
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
pub enum LayoutOperation { pub enum LayoutOperation {
/// Assign role to Garage node /// Assign role to Garage node
#[structopt(name = "assign", version = garage_version())] #[structopt(name = "assign")]
Assign(AssignRoleOpt), Assign(AssignRoleOpt),
/// Remove role from Garage cluster node /// Remove role from Garage cluster node
#[structopt(name = "remove", version = garage_version())] #[structopt(name = "remove")]
Remove(RemoveRoleOpt), Remove(RemoveRoleOpt),
/// Show roles currently assigned to nodes and changes staged for commit /// Show roles currently assigned to nodes and changes staged for commit
#[structopt(name = "show", version = garage_version())] #[structopt(name = "show")]
Show, Show,
/// Apply staged changes to cluster layout /// Apply staged changes to cluster layout
#[structopt(name = "apply", version = garage_version())] #[structopt(name = "apply")]
Apply(ApplyLayoutOpt), Apply(ApplyLayoutOpt),
/// Revert staged changes to cluster layout /// Revert staged changes to cluster layout
#[structopt(name = "revert", version = garage_version())] #[structopt(name = "revert")]
Revert(RevertLayoutOpt), Revert(RevertLayoutOpt),
} }
@@ -151,43 +150,43 @@ pub struct RevertLayoutOpt {
#[derive(Serialize, Deserialize, StructOpt, Debug)] #[derive(Serialize, Deserialize, StructOpt, Debug)]
pub enum BucketOperation { pub enum BucketOperation {
/// List buckets /// List buckets
#[structopt(name = "list", version = garage_version())] #[structopt(name = "list")]
List, List,
/// Get bucket info /// Get bucket info
#[structopt(name = "info", version = garage_version())] #[structopt(name = "info")]
Info(BucketOpt), Info(BucketOpt),
/// Create bucket /// Create bucket
#[structopt(name = "create", version = garage_version())] #[structopt(name = "create")]
Create(BucketOpt), Create(BucketOpt),
/// Delete bucket /// Delete bucket
#[structopt(name = "delete", version = garage_version())] #[structopt(name = "delete")]
Delete(DeleteBucketOpt), Delete(DeleteBucketOpt),
/// Alias bucket under new name /// Alias bucket under new name
#[structopt(name = "alias", version = garage_version())] #[structopt(name = "alias")]
Alias(AliasBucketOpt), Alias(AliasBucketOpt),
/// Remove bucket alias /// Remove bucket alias
#[structopt(name = "unalias", version = garage_version())] #[structopt(name = "unalias")]
Unalias(UnaliasBucketOpt), Unalias(UnaliasBucketOpt),
/// Allow key to read or write to bucket /// Allow key to read or write to bucket
#[structopt(name = "allow", version = garage_version())] #[structopt(name = "allow")]
Allow(PermBucketOpt), Allow(PermBucketOpt),
/// Deny key from reading or writing to bucket /// Deny key from reading or writing to bucket
#[structopt(name = "deny", version = garage_version())] #[structopt(name = "deny")]
Deny(PermBucketOpt), Deny(PermBucketOpt),
/// Expose as website or not /// Expose as website or not
#[structopt(name = "website", version = garage_version())] #[structopt(name = "website")]
Website(WebsiteOpt), Website(WebsiteOpt),
/// Set the quotas for this bucket /// Set the quotas for this bucket
#[structopt(name = "set-quotas", version = garage_version())] #[structopt(name = "set-quotas")]
SetQuotas(SetQuotasOpt), SetQuotas(SetQuotasOpt),
} }
@@ -293,35 +292,35 @@ pub struct SetQuotasOpt {
#[derive(Serialize, Deserialize, StructOpt, Debug)] #[derive(Serialize, Deserialize, StructOpt, Debug)]
pub enum KeyOperation { pub enum KeyOperation {
/// List keys /// List keys
#[structopt(name = "list", version = garage_version())] #[structopt(name = "list")]
List, List,
/// Get key info /// Get key info
#[structopt(name = "info", version = garage_version())] #[structopt(name = "info")]
Info(KeyOpt), Info(KeyOpt),
/// Create new key /// Create new key
#[structopt(name = "new", version = garage_version())] #[structopt(name = "new")]
New(KeyNewOpt), New(KeyNewOpt),
/// Rename key /// Rename key
#[structopt(name = "rename", version = garage_version())] #[structopt(name = "rename")]
Rename(KeyRenameOpt), Rename(KeyRenameOpt),
/// Delete key /// Delete key
#[structopt(name = "delete", version = garage_version())] #[structopt(name = "delete")]
Delete(KeyDeleteOpt), Delete(KeyDeleteOpt),
/// Set permission flags for key /// Set permission flags for key
#[structopt(name = "allow", version = garage_version())] #[structopt(name = "allow")]
Allow(KeyPermOpt), Allow(KeyPermOpt),
/// Unset permission flags for key /// Unset permission flags for key
#[structopt(name = "deny", version = garage_version())] #[structopt(name = "deny")]
Deny(KeyPermOpt), Deny(KeyPermOpt),
/// Import key /// Import key
#[structopt(name = "import", version = garage_version())] #[structopt(name = "import")]
Import(KeyImportOpt), Import(KeyImportOpt),
} }
@@ -393,7 +392,7 @@ pub struct MigrateOpt {
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)] #[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
pub enum MigrateWhat { pub enum MigrateWhat {
/// Migrate buckets and permissions from v0.5.0 /// Migrate buckets and permissions from v0.5.0
#[structopt(name = "buckets050", version = garage_version())] #[structopt(name = "buckets050")]
Buckets050, Buckets050,
} }
@@ -414,19 +413,19 @@ pub struct RepairOpt {
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)] #[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
pub enum RepairWhat { pub enum RepairWhat {
/// Only do a full sync of metadata tables /// Only do a full sync of metadata tables
#[structopt(name = "tables", version = garage_version())] #[structopt(name = "tables")]
Tables, Tables,
/// Only repair (resync/rebalance) the set of stored blocks /// Only repair (resync/rebalance) the set of stored blocks
#[structopt(name = "blocks", version = garage_version())] #[structopt(name = "blocks")]
Blocks, Blocks,
/// Only redo the propagation of object deletions to the version table (slow) /// Only redo the propagation of object deletions to the version table (slow)
#[structopt(name = "versions", version = garage_version())] #[structopt(name = "versions")]
Versions, Versions,
/// Only redo the propagation of version deletions to the block ref table (extremely slow) /// Only redo the propagation of version deletions to the block ref table (extremely slow)
#[structopt(name = "block_refs", version = garage_version())] #[structopt(name = "block_refs")]
BlockRefs, BlockRefs,
/// Verify integrity of all blocks on disc (extremely slow, i/o intensive) /// Verify integrity of all blocks on disc (extremely slow, i/o intensive)
#[structopt(name = "scrub", version = garage_version())] #[structopt(name = "scrub")]
Scrub { Scrub {
#[structopt(subcommand)] #[structopt(subcommand)]
cmd: ScrubCmd, cmd: ScrubCmd,
@@ -436,19 +435,19 @@ pub enum RepairWhat {
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)] #[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
pub enum ScrubCmd { pub enum ScrubCmd {
/// Start scrub /// Start scrub
#[structopt(name = "start", version = garage_version())] #[structopt(name = "start")]
Start, Start,
/// Pause scrub (it will resume automatically after 24 hours) /// Pause scrub (it will resume automatically after 24 hours)
#[structopt(name = "pause", version = garage_version())] #[structopt(name = "pause")]
Pause, Pause,
/// Resume paused scrub /// Resume paused scrub
#[structopt(name = "resume", version = garage_version())] #[structopt(name = "resume")]
Resume, Resume,
/// Cancel scrub in progress /// Cancel scrub in progress
#[structopt(name = "cancel", version = garage_version())] #[structopt(name = "cancel")]
Cancel, Cancel,
/// Set tranquility level for in-progress and future scrubs /// Set tranquility level for in-progress and future scrubs
#[structopt(name = "set-tranquility", version = garage_version())] #[structopt(name = "set-tranquility")]
SetTranquility { SetTranquility {
#[structopt()] #[structopt()]
tranquility: u32, tranquility: u32,
@@ -469,10 +468,10 @@ pub struct OfflineRepairOpt {
pub enum OfflineRepairWhat { pub enum OfflineRepairWhat {
/// Repair K2V item counters /// Repair K2V item counters
#[cfg(feature = "k2v")] #[cfg(feature = "k2v")]
#[structopt(name = "k2v_item_counters", version = garage_version())] #[structopt(name = "k2v_item_counters")]
K2VItemCounters, K2VItemCounters,
/// Repair object counters /// Repair object counters
#[structopt(name = "object_counters", version = garage_version())] #[structopt(name = "object_counters")]
ObjectCounters, ObjectCounters,
} }
@@ -496,17 +495,11 @@ pub struct WorkerOpt {
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)] #[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
pub enum WorkerCmd { pub enum WorkerCmd {
/// List all workers on Garage node /// List all workers on Garage node
#[structopt(name = "list", version = garage_version())] #[structopt(name = "list")]
List { List {
#[structopt(flatten)] #[structopt(flatten)]
opt: WorkerListOpt, opt: WorkerListOpt,
}, },
/// Set worker parameter
#[structopt(name = "set", version = garage_version())]
Set {
#[structopt(subcommand)]
opt: WorkerSetCmd,
},
} }
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone, Copy)] #[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone, Copy)]
@@ -518,16 +511,3 @@ pub struct WorkerListOpt {
#[structopt(short = "e", long = "errors")] #[structopt(short = "e", long = "errors")]
pub errors: bool, pub errors: bool,
} }
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
pub enum WorkerSetCmd {
/// Set tranquility of scrub operations
#[structopt(name = "scrub-tranquility", version = garage_version())]
ScrubTranquility { tranquility: u32 },
/// Set number of concurrent block resync workers
#[structopt(name = "resync-n-workers", version = garage_version())]
ResyncNWorkers { n_workers: usize },
/// Set tranquility of block resync operations
#[structopt(name = "resync-tranquility", version = garage_version())]
ResyncTranquility { tranquility: u32 },
}
+3 -49
View File
@@ -8,15 +8,8 @@ mod admin;
mod cli; mod cli;
mod repair; mod repair;
mod server; mod server;
#[cfg(feature = "telemetry-otlp")]
mod tracing_setup; 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::net::SocketAddr;
use std::path::PathBuf; use std::path::PathBuf;
@@ -36,10 +29,7 @@ use admin::*;
use cli::*; use cli::*;
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
#[structopt( #[structopt(name = "garage")]
name = "garage",
about = "S3-compatible object store for self-hosted geo-distributed deployments"
)]
struct Opt { struct Opt {
/// Host to connect to for admin operations, in the format: /// Host to connect to for admin operations, in the format:
/// <public-key>@<ip>:<port> /// <public-key>@<ip>:<port>
@@ -68,10 +58,7 @@ async fn main() {
if std::env::var("RUST_LOG").is_err() { if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "netapp=info,garage=info") std::env::set_var("RUST_LOG", "netapp=info,garage=info")
} }
tracing_subscriber::fmt() pretty_env_logger::init();
.with_writer(std::io::stderr)
.with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env())
.init();
sodiumoxide::init().expect("Unable to init sodiumoxide"); sodiumoxide::init().expect("Unable to init sodiumoxide");
// Abort on panic (same behavior as in Go) // Abort on panic (same behavior as in Go)
@@ -80,40 +67,7 @@ async fn main() {
std::process::abort(); std::process::abort();
})); }));
// Initialize version and features info let opt = Opt::from_args();
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",
][..];
if let Some(git_version) = option_env!("GIT_VERSION") {
garage_util::version::init_version(git_version);
}
garage_util::version::init_features(features);
// Parse arguments
let version = format!(
"{} [features: {}]",
garage_util::version::garage_version(),
features.join(", ")
);
let opt = Opt::from_clap(&Opt::clap().version(version.as_str()).get_matches());
let res = match opt.cmd { let res = match opt.cmd {
Command::Server => server::run_server(opt.config_file).await, Command::Server => server::run_server(opt.config_file).await,
Command::OfflineRepair(repair_opt) => { Command::OfflineRepair(repair_opt) => {
+44 -77
View File
@@ -9,13 +9,12 @@ use garage_util::error::Error;
use garage_api::admin::api_server::AdminApiServer; use garage_api::admin::api_server::AdminApiServer;
use garage_api::s3::api_server::S3ApiServer; use garage_api::s3::api_server::S3ApiServer;
use garage_model::garage::Garage; use garage_model::garage::Garage;
use garage_web::WebServer; use garage_web::run_web_server;
#[cfg(feature = "k2v")] #[cfg(feature = "k2v")]
use garage_api::k2v::api_server::K2VApiServer; use garage_api::k2v::api_server::K2VApiServer;
use crate::admin::*; use crate::admin::*;
#[cfg(feature = "telemetry-otlp")]
use crate::tracing_setup::*; use crate::tracing_setup::*;
async fn wait_from(mut chan: watch::Receiver<bool>) { async fn wait_from(mut chan: watch::Receiver<bool>) {
@@ -30,8 +29,6 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> {
info!("Loading configuration..."); info!("Loading configuration...");
let config = read_config(config_file)?; let config = read_config(config_file)?;
// ---- Initialize Garage internals ----
info!("Initializing background runner..."); info!("Initializing background runner...");
let watch_cancel = netapp::util::watch_ctrl_c(); let watch_cancel = netapp::util::watch_ctrl_c();
let (background, await_background_done) = BackgroundRunner::new(16, watch_cancel.clone()); let (background, await_background_done) = BackgroundRunner::new(16, watch_cancel.clone());
@@ -39,14 +36,9 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> {
info!("Initializing Garage main data store..."); info!("Initializing Garage main data store...");
let garage = Garage::new(config.clone(), background)?; let garage = Garage::new(config.clone(), background)?;
if config.admin.trace_sink.is_some() { info!("Initialize tracing...");
info!("Initialize tracing..."); if let Some(export_to) = config.admin.trace_sink {
init_tracing(&export_to, garage.system.id)?;
#[cfg(feature = "telemetry-otlp")]
init_tracing(config.admin.trace_sink.as_ref().unwrap(), garage.system.id)?;
#[cfg(not(feature = "telemetry-otlp"))]
error!("Garage was built without OTLP exporter, admin.trace_sink is ignored.");
} }
info!("Initialize Admin API server and metrics collector..."); info!("Initialize Admin API server and metrics collector...");
@@ -58,78 +50,53 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> {
info!("Create admin RPC handler..."); info!("Create admin RPC handler...");
AdminRpcHandler::new(garage.clone()); AdminRpcHandler::new(garage.clone());
// ---- Launch public-facing API servers ---- info!("Initializing S3 API server...");
let s3_api_server = tokio::spawn(S3ApiServer::run(
garage.clone(),
wait_from(watch_cancel.clone()),
));
let mut servers = vec![]; #[cfg(feature = "k2v")]
let k2v_api_server = {
info!("Initializing K2V API server...");
tokio::spawn(K2VApiServer::run(
garage.clone(),
wait_from(watch_cancel.clone()),
))
};
if let Some(s3_bind_addr) = &config.s3_api.api_bind_addr { info!("Initializing web server...");
info!("Initializing S3 API server..."); let web_server = tokio::spawn(run_web_server(
servers.push(( garage.clone(),
"S3 API", wait_from(watch_cancel.clone()),
tokio::spawn(S3ApiServer::run( ));
garage.clone(),
*s3_bind_addr,
config.s3_api.s3_region.clone(),
wait_from(watch_cancel.clone()),
)),
));
}
if config.k2v_api.is_some() { info!("Launching Admin API server...");
#[cfg(feature = "k2v")] let admin_server = tokio::spawn(admin_server.run(wait_from(watch_cancel.clone())));
{
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()))),
));
}
#[cfg(not(feature = "metrics"))]
if config.admin.metrics_token.is_some() {
warn!("This Garage version is built without the metrics feature");
}
// Stuff runs // Stuff runs
// When a cancel signal is sent, stuff stops // When a cancel signal is sent, stuff stops
if let Err(e) = s3_api_server.await? {
// Collect stuff warn!("S3 API server exited with error: {}", e);
for (desc, join_handle) in servers { } else {
if let Err(e) = join_handle.await? { info!("S3 API server exited without error.");
error!("{} server exited with error: {}", desc, e); }
} else { #[cfg(feature = "k2v")]
info!("{} server exited without error.", desc); 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.");
} }
// Remove RPC handlers for system to break reference cycles // Remove RPC handlers for system to break reference cycles
+1 -4
View File
@@ -3,8 +3,5 @@ mod common;
mod admin; mod admin;
mod bucket; mod bucket;
mod s3;
#[cfg(feature = "k2v")]
mod k2v; mod k2v;
mod s3;
+1 -1
View File
@@ -22,7 +22,7 @@ tokio = "1.17.0"
# cli deps # cli deps
clap = { version = "3.1.18", optional = true, features = ["derive", "env"] } clap = { version = "3.1.18", optional = true, features = ["derive", "env"] }
garage_util = { version = "0.8.0", path = "../util", optional = true } garage_util = { version = "0.7.0", path = "../util", optional = true }
[features] [features]
+9 -9
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_model" name = "garage_model"
version = "0.8.0" version = "0.7.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
@@ -15,10 +15,11 @@ path = "lib.rs"
[dependencies] [dependencies]
garage_db = { version = "0.8.0", path = "../db" } garage_db = { version = "0.8.0", path = "../db" }
garage_rpc = { version = "0.8.0", path = "../rpc" } garage_rpc = { version = "0.7.0", path = "../rpc" }
garage_table = { version = "0.8.0", path = "../table" } garage_table = { version = "0.7.0", path = "../table" }
garage_block = { version = "0.8.0", path = "../block" } garage_block = { version = "0.7.0", path = "../block" }
garage_util = { version = "0.8.0", path = "../util" } garage_util = { version = "0.7.0", path = "../util" }
garage_model_050 = { package = "garage_model", version = "0.5.1" }
async-trait = "0.1.7" async-trait = "0.1.7"
arc-swap = "1.0" arc-swap = "1.0"
@@ -39,10 +40,9 @@ 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 = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
opentelemetry = "0.17" opentelemetry = "0.17"
netapp = "0.5" #netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
#netapp = { version = "0.4", path = "../../../netapp" }
netapp = "0.4"
[features] [features]
k2v = [ "garage_util/k2v" ] k2v = [ "garage_util/k2v" ]
lmdb = [ "garage_db/lmdb" ]
sled = [ "garage_db/sled" ]
sqlite = [ "garage_db/sqlite" ]
+1 -1
View File
@@ -7,7 +7,7 @@ use garage_table::*;
/// The bucket alias table holds the names given to buckets /// The bucket alias table holds the names given to buckets
/// in the global namespace. /// in the global namespace.
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub struct BucketAlias { pub struct BucketAlias {
name: String, name: String,
pub state: crdt::Lww<Option<Uuid>>, pub state: crdt::Lww<Option<Uuid>>,
+2 -2
View File
@@ -12,7 +12,7 @@ use crate::permission::BucketKeyPerm;
/// Its parameters are not directly accessible as: /// Its parameters are not directly accessible as:
/// - It must be possible to merge paramaters, hence the use of a LWW CRDT. /// - 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. /// - A bucket has 2 states, Present or Deleted and parameters make sense only if present.
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub struct Bucket { pub struct Bucket {
/// ID of the bucket /// ID of the bucket
pub id: Uuid, pub id: Uuid,
@@ -21,7 +21,7 @@ pub struct Bucket {
} }
/// Configuration for a bucket /// Configuration for a bucket
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub struct BucketParams { pub struct BucketParams {
/// Bucket's creation date /// Bucket's creation date
pub creation_date: u64, pub creation_date: u64,
+11 -46
View File
@@ -6,7 +6,7 @@ use garage_db as db;
use garage_util::background::*; use garage_util::background::*;
use garage_util::config::*; use garage_util::config::*;
use garage_util::error::*; use garage_util::error::Error;
use garage_rpc::system::System; use garage_rpc::system::System;
@@ -76,17 +76,10 @@ pub struct GarageK2V {
impl Garage { impl Garage {
/// Create and run garage /// Create and run garage
pub fn new(config: Config, background: Arc<BackgroundRunner>) -> Result<Arc<Self>, Error> { pub fn new(config: Config, background: Arc<BackgroundRunner>) -> Result<Arc<Self>, 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..."); info!("Opening database...");
let mut db_path = config.metadata_dir.clone(); 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() { let db = match config.db_engine.as_str() {
// ---- Sled DB ----
#[cfg(feature = "sled")]
"sled" => { "sled" => {
db_path.push("db"); db_path.push("db");
info!("Opening Sled database at: {}", db_path.display()); info!("Opening Sled database at: {}", db_path.display());
@@ -98,10 +91,6 @@ impl Garage {
.expect("Unable to open sled DB"); .expect("Unable to open sled DB");
db::sled_adapter::SledDb::init(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" => { "sqlite" | "sqlite3" | "rusqlite" => {
db_path.push("db.sqlite"); db_path.push("db.sqlite");
info!("Opening Sqlite database at: {}", db_path.display()); info!("Opening Sqlite database at: {}", db_path.display());
@@ -109,48 +98,23 @@ impl Garage {
.expect("Unable to open sqlite DB"); .expect("Unable to open sqlite DB");
db::sqlite_adapter::SqliteDb::init(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" => { "lmdb" | "heed" => {
db_path.push("db.lmdb"); db_path.push("db.lmdb");
info!("Opening LMDB database at: {}", db_path.display()); info!("Opening LMDB database at: {}", db_path.display());
std::fs::create_dir_all(&db_path).expect("Unable to create LMDB data directory"); 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 map_size = garage_db::lmdb_adapter::recommended_map_size();
use db::lmdb_adapter::heed; let db = db::lmdb_adapter::heed::EnvOpenOptions::new()
let mut env_builder = heed::EnvOpenOptions::new(); .max_dbs(100)
env_builder.max_dbs(100); .map_size(map_size)
env_builder.max_readers(500); .open(&db_path)
env_builder.map_size(map_size); .expect("Unable to open LMDB DB");
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) 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 => { e => {
return Err(Error::Message(format!( return Err(Error::Message(format!(
"Unsupported DB engine: {} (options: {})", "Unsupported DB engine: {} (options: sled, sqlite, lmdb)",
e, e
vec![
#[cfg(feature = "sled")]
"sled",
#[cfg(feature = "sqlite")]
"sqlite",
#[cfg(feature = "lmdb")]
"lmdb",
]
.join(", ")
))); )));
} }
}; };
@@ -169,7 +133,7 @@ impl Garage {
background.clone(), background.clone(),
replication_mode.replication_factor(), replication_mode.replication_factor(),
&config, &config,
)?; );
let data_rep_param = TableShardedReplication { let data_rep_param = TableShardedReplication {
system: system.clone(), system: system.clone(),
@@ -195,6 +159,7 @@ impl Garage {
&db, &db,
config.data_dir.clone(), config.data_dir.clone(),
config.compression_level, config.compression_level,
config.block_manager_background_tranquility,
data_rep_param, data_rep_param,
system.clone(), system.clone(),
); );
+1 -1
View File
@@ -81,7 +81,7 @@ impl<T: CountedItem> CounterEntry<T> {
} }
/// A counter entry in the global table /// A counter entry in the global table
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub struct CounterValue { pub struct CounterValue {
pub node_values: BTreeMap<Uuid, (u64, i64)>, pub node_values: BTreeMap<Uuid, (u64, i64)>,
} }
+3 -3
View File
@@ -6,10 +6,10 @@ use garage_util::data::*;
use crate::permission::BucketKeyPerm; use crate::permission::BucketKeyPerm;
use crate::prev::v051::key_table as old; use garage_model_050::key_table as old;
/// An api key /// An api key
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub struct Key { pub struct Key {
/// The id of the key (immutable), used as partition key /// The id of the key (immutable), used as partition key
pub key_id: String, pub key_id: String,
@@ -19,7 +19,7 @@ pub struct Key {
} }
/// Configuration for a key /// Configuration for a key
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub struct KeyParams { pub struct KeyParams {
/// The secret_key associated (immutable) /// The secret_key associated (immutable)
pub secret_key: String, pub secret_key: String,
-3
View File
@@ -1,9 +1,6 @@
#[macro_use] #[macro_use]
extern crate tracing; extern crate tracing;
// For migration from previous versions
pub(crate) mod prev;
pub mod permission; pub mod permission;
pub mod index_counter; pub mod index_counter;
+1 -1
View File
@@ -5,7 +5,7 @@ use garage_util::data::*;
use garage_util::error::Error as GarageError; use garage_util::error::Error as GarageError;
use garage_util::time::*; use garage_util::time::*;
use crate::prev::v051::bucket_table as old_bucket; use garage_model_050::bucket_table as old_bucket;
use crate::bucket_alias_table::*; use crate::bucket_alias_table::*;
use crate::bucket_table::*; use crate::bucket_table::*;
-1
View File
@@ -1 +0,0 @@
pub(crate) mod v051;
-63
View File
@@ -1,63 +0,0 @@
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, Eq, 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<BucketState>,
}
/// State of a bucket
#[derive(PartialEq, Eq, 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, 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<String, PermissionSet>,
/// Is the bucket served as http
pub website: crdt::Lww<bool>,
}
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);
}
}
-50
View File
@@ -1,50 +0,0 @@
use serde::{Deserialize, Serialize};
use garage_table::crdt::*;
use garage_table::*;
/// An api key
#[derive(PartialEq, Eq, 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<String>,
/// 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<String, PermissionSet>,
}
/// 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);
}
}
}
-4
View File
@@ -1,4 +0,0 @@
pub(crate) mod bucket_table;
pub(crate) mod key_table;
pub(crate) mod object_table;
pub(crate) mod version_table;
-149
View File
@@ -1,149 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use garage_util::data::*;
use garage_table::crdt::*;
/// An object
#[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,
/// 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<ObjectVersion>,
}
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, Eq, 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, Eq, 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<u8>),
/// 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<String, String>,
}
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::<Vec<_>>();
}
}
}
-79
View File
@@ -1,79 +0,0 @@
use serde::{Deserialize, Serialize};
use garage_util::data::*;
use garage_table::crdt::*;
use garage_table::*;
/// A version of an object
#[derive(PartialEq, Eq, 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<VersionBlockKey, VersionBlock>,
/// Etag of each part in case of a multipart upload, empty otherwise
pub parts_etags: crdt::Map<u64, String>,
// 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<std::cmp::Ordering> {
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);
}
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ use garage_table::*;
use garage_block::manager::*; use garage_block::manager::*;
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub struct BlockRef { pub struct BlockRef {
/// Hash (blake2 sum) of the block, used as partition key /// Hash (blake2 sum) of the block, used as partition key
pub block: Hash, pub block: Hash,
+4 -4
View File
@@ -14,14 +14,14 @@ use garage_table::*;
use crate::index_counter::*; use crate::index_counter::*;
use crate::s3::version_table::*; use crate::s3::version_table::*;
use crate::prev::v051::object_table as old; use garage_model_050::object_table as old;
pub const OBJECTS: &str = "objects"; pub const OBJECTS: &str = "objects";
pub const UNFINISHED_UPLOADS: &str = "unfinished_uploads"; pub const UNFINISHED_UPLOADS: &str = "unfinished_uploads";
pub const BYTES: &str = "bytes"; pub const BYTES: &str = "bytes";
/// An object /// An object
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub struct Object { pub struct Object {
/// The bucket in which the object is stored, used as partition key /// The bucket in which the object is stored, used as partition key
pub bucket_id: Uuid, pub bucket_id: Uuid,
@@ -70,7 +70,7 @@ impl Object {
} }
/// Informations about a version of an object /// Informations about a version of an object
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub struct ObjectVersion { pub struct ObjectVersion {
/// Id of the version /// Id of the version
pub uuid: Uuid, pub uuid: Uuid,
@@ -81,7 +81,7 @@ pub struct ObjectVersion {
} }
/// State of an object version /// State of an object version
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum ObjectVersionState { pub enum ObjectVersionState {
/// The version is being received /// The version is being received
Uploading(ObjectVersionHeaders), Uploading(ObjectVersionHeaders),
+2 -2
View File
@@ -12,10 +12,10 @@ use garage_table::*;
use crate::s3::block_ref_table::*; use crate::s3::block_ref_table::*;
use crate::prev::v051::version_table as old; use garage_model_050::version_table as old;
/// A version of an object /// A version of an object
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub struct Version { pub struct Version {
/// UUID of the version, used as partition key /// UUID of the version, used as partition key
pub uuid: Uuid, pub uuid: Uuid,
+6 -4
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_rpc" name = "garage_rpc"
version = "0.8.0" version = "0.7.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
@@ -14,11 +14,12 @@ path = "lib.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
garage_util = { version = "0.8.0", path = "../util" } garage_util = { version = "0.7.0", path = "../util" }
arc-swap = "1.0" arc-swap = "1.0"
bytes = "1.0" bytes = "1.0"
gethostname = "0.2" gethostname = "0.2"
git-version = "0.3.4"
hex = "0.4" hex = "0.4"
tracing = "0.1.30" tracing = "0.1.30"
rand = "0.8" rand = "0.8"
@@ -45,11 +46,12 @@ tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi
tokio-stream = { version = "0.1", features = ["net"] } tokio-stream = { version = "0.1", features = ["net"] }
opentelemetry = "0.17" opentelemetry = "0.17"
netapp = { version = "0.5.0", features = ["telemetry"] } #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"] }
hyper = { version = "0.14", features = ["client", "http1", "runtime", "tcp"] } hyper = { version = "0.14", features = ["client", "http1", "runtime", "tcp"] }
[features] [features]
kubernetes-discovery = [ "kube", "k8s-openapi", "openssl", "schemars" ] kubernetes-discovery = [ "kube", "k8s-openapi", "openssl", "schemars" ]
system-libs = [ "sodiumoxide/use-pkg-config" ]
+1 -1
View File
@@ -56,7 +56,7 @@ pub async fn get_kubernetes_nodes(
let mut ret = Vec::with_capacity(nodes.items.len()); let mut ret = Vec::with_capacity(nodes.items.len());
for node in nodes { for node in nodes {
info!("Found Pod: {:?}", node.metadata.name); println!("Found Pod: {:?}", node.metadata.name);
let pubkey = &node let pubkey = &node
.metadata .metadata
+89 -100
View File
@@ -15,14 +15,10 @@ use opentelemetry::{
Context, Context,
}; };
pub use netapp::endpoint::{Endpoint, EndpointHandler, StreamingEndpointHandler}; pub use netapp::endpoint::{Endpoint, EndpointHandler, Message as Rpc};
use netapp::message::IntoReq;
pub use netapp::message::{
Message as Rpc, OrderTag, Req, RequestPriority, Resp, PRIO_BACKGROUND, PRIO_HIGH, PRIO_NORMAL,
PRIO_SECONDARY,
};
use netapp::peering::fullmesh::FullMeshPeeringStrategy; use netapp::peering::fullmesh::FullMeshPeeringStrategy;
pub use netapp::{self, NetApp, NodeID}; pub use netapp::proto::*;
pub use netapp::{NetApp, NodeID};
use garage_util::background::BackgroundRunner; use garage_util::background::BackgroundRunner;
use garage_util::data::*; use garage_util::data::*;
@@ -32,10 +28,12 @@ use garage_util::metrics::RecordDuration;
use crate::metrics::RpcMetrics; use crate::metrics::RpcMetrics;
use crate::ring::Ring; use crate::ring::Ring;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30); const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
// Don't allow more than 100 concurrent outgoing RPCs. // Try to never have more than 200MB of outgoing requests
const MAX_CONCURRENT_REQUESTS: usize = 100; // buffered at the same time. Other requests are queued until
// space is freed.
const REQUEST_BUFFER_SIZE: usize = 200 * 1024 * 1024;
/// Strategy to apply when making RPC /// Strategy to apply when making RPC
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
@@ -97,7 +95,7 @@ impl RpcHelper {
background: Arc<BackgroundRunner>, background: Arc<BackgroundRunner>,
ring: watch::Receiver<Arc<Ring>>, ring: watch::Receiver<Arc<Ring>>,
) -> Self { ) -> Self {
let sem = Arc::new(Semaphore::new(MAX_CONCURRENT_REQUESTS)); let sem = Arc::new(Semaphore::new(REQUEST_BUFFER_SIZE));
let metrics = RpcMetrics::new(sem.clone()); let metrics = RpcMetrics::new(sem.clone());
@@ -111,17 +109,30 @@ impl RpcHelper {
})) }))
} }
pub async fn call<M, N, H, S>( pub async fn call<M, H, S>(
&self, &self,
endpoint: &Endpoint<M, H>, endpoint: &Endpoint<M, H>,
to: Uuid, to: Uuid,
msg: N, msg: M,
strat: RequestStrategy, strat: RequestStrategy,
) -> Result<S, Error> ) -> Result<S, Error>
where where
M: Rpc<Response = Result<S, Error>>, M: Rpc<Response = Result<S, Error>>,
N: IntoReq<M> + Send, H: EndpointHandler<M>,
H: StreamingEndpointHandler<M>, {
self.call_arc(endpoint, to, Arc::new(msg), strat).await
}
pub async fn call_arc<M, H, S>(
&self,
endpoint: &Endpoint<M, H>,
to: Uuid,
msg: Arc<M>,
strat: RequestStrategy,
) -> Result<S, Error>
where
M: Rpc<Response = Result<S, Error>>,
H: EndpointHandler<M>,
{ {
let metric_tags = [ let metric_tags = [
KeyValue::new("rpc_endpoint", endpoint.path().to_string()), KeyValue::new("rpc_endpoint", endpoint.path().to_string()),
@@ -129,10 +140,11 @@ impl RpcHelper {
KeyValue::new("to", format!("{:?}", to)), KeyValue::new("to", format!("{:?}", to)),
]; ];
let msg_size = rmp_to_vec_all_named(&msg)?.len() as u32;
let permit = self let permit = self
.0 .0
.request_buffer_semaphore .request_buffer_semaphore
.acquire() .acquire_many(msg_size)
.record_duration(&self.0.metrics.rpc_queueing_time, &metric_tags) .record_duration(&self.0.metrics.rpc_queueing_time, &metric_tags)
.await?; .await?;
@@ -140,7 +152,7 @@ impl RpcHelper {
let node_id = to.into(); let node_id = to.into();
let rpc_call = endpoint let rpc_call = endpoint
.call_streaming(&node_id, msg, strat.rs_priority) .call(&node_id, msg, strat.rs_priority)
.record_duration(&self.0.metrics.rpc_duration, &metric_tags); .record_duration(&self.0.metrics.rpc_duration, &metric_tags);
select! { select! {
@@ -150,7 +162,7 @@ impl RpcHelper {
if res.is_err() { if res.is_err() {
self.0.metrics.rpc_netapp_error_counter.add(1, &metric_tags); self.0.metrics.rpc_netapp_error_counter.add(1, &metric_tags);
} }
let res = res?.into_msg(); let res = res?;
if res.is_err() { if res.is_err() {
self.0.metrics.rpc_garage_error_counter.add(1, &metric_tags); self.0.metrics.rpc_garage_error_counter.add(1, &metric_tags);
@@ -166,42 +178,38 @@ impl RpcHelper {
} }
} }
pub async fn call_many<M, N, H, S>( pub async fn call_many<M, H, S>(
&self, &self,
endpoint: &Endpoint<M, H>, endpoint: &Endpoint<M, H>,
to: &[Uuid], to: &[Uuid],
msg: N, msg: M,
strat: RequestStrategy, strat: RequestStrategy,
) -> Result<Vec<(Uuid, Result<S, Error>)>, Error> ) -> Vec<(Uuid, Result<S, Error>)>
where where
M: Rpc<Response = Result<S, Error>>, M: Rpc<Response = Result<S, Error>>,
N: IntoReq<M>, H: EndpointHandler<M>,
H: StreamingEndpointHandler<M>,
{ {
let msg = msg.into_req().map_err(netapp::error::Error::from)?; let msg = Arc::new(msg);
let resps = join_all( let resps = join_all(
to.iter() to.iter()
.map(|to| self.call(endpoint, *to, msg.clone(), strat)), .map(|to| self.call_arc(endpoint, *to, msg.clone(), strat)),
) )
.await; .await;
Ok(to to.iter()
.iter()
.cloned() .cloned()
.zip(resps.into_iter()) .zip(resps.into_iter())
.collect::<Vec<_>>()) .collect::<Vec<_>>()
} }
pub async fn broadcast<M, N, H, S>( pub async fn broadcast<M, H, S>(
&self, &self,
endpoint: &Endpoint<M, H>, endpoint: &Endpoint<M, H>,
msg: N, msg: M,
strat: RequestStrategy, strat: RequestStrategy,
) -> Result<Vec<(Uuid, Result<S, Error>)>, Error> ) -> Vec<(Uuid, Result<S, Error>)>
where where
M: Rpc<Response = Result<S, Error>>, M: Rpc<Response = Result<S, Error>>,
N: IntoReq<M>, H: EndpointHandler<M>,
H: StreamingEndpointHandler<M>,
{ {
let to = self let to = self
.0 .0
@@ -215,17 +223,16 @@ impl RpcHelper {
/// Make a RPC call to multiple servers, returning either a Vec of responses, /// 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 /// or an error if quorum could not be reached due to too many errors
pub async fn try_call_many<M, N, H, S>( pub async fn try_call_many<M, H, S>(
&self, &self,
endpoint: &Arc<Endpoint<M, H>>, endpoint: &Arc<Endpoint<M, H>>,
to: &[Uuid], to: &[Uuid],
msg: N, msg: M,
strategy: RequestStrategy, strategy: RequestStrategy,
) -> Result<Vec<S>, Error> ) -> Result<Vec<S>, Error>
where where
M: Rpc<Response = Result<S, Error>> + 'static, M: Rpc<Response = Result<S, Error>> + 'static,
N: IntoReq<M>, H: EndpointHandler<M> + 'static,
H: StreamingEndpointHandler<M> + 'static,
S: Send + 'static, S: Send + 'static,
{ {
let quorum = strategy.rs_quorum.unwrap_or(to.len()); let quorum = strategy.rs_quorum.unwrap_or(to.len());
@@ -255,21 +262,20 @@ impl RpcHelper {
.await .await
} }
async fn try_call_many_internal<M, N, H, S>( async fn try_call_many_internal<M, H, S>(
&self, &self,
endpoint: &Arc<Endpoint<M, H>>, endpoint: &Arc<Endpoint<M, H>>,
to: &[Uuid], to: &[Uuid],
msg: N, msg: M,
strategy: RequestStrategy, strategy: RequestStrategy,
quorum: usize, quorum: usize,
) -> Result<Vec<S>, Error> ) -> Result<Vec<S>, Error>
where where
M: Rpc<Response = Result<S, Error>> + 'static, M: Rpc<Response = Result<S, Error>> + 'static,
N: IntoReq<M>, H: EndpointHandler<M> + 'static,
H: StreamingEndpointHandler<M> + 'static,
S: Send + 'static, S: Send + 'static,
{ {
let msg = msg.into_req().map_err(netapp::error::Error::from)?; let msg = Arc::new(msg);
// Build future for each request // Build future for each request
// They are not started now: they are added below in a FuturesUnordered // They are not started now: they are added below in a FuturesUnordered
@@ -279,7 +285,7 @@ impl RpcHelper {
let msg = msg.clone(); let msg = msg.clone();
let endpoint2 = endpoint.clone(); let endpoint2 = endpoint.clone();
(to, async move { (to, async move {
self2.call(&endpoint2, to, msg, strategy).await self2.call_arc(&endpoint2, to, msg, strategy).await
}) })
}); });
@@ -293,19 +299,47 @@ impl RpcHelper {
// to reach a quorum, priorizing nodes with the lowest latency. // to reach a quorum, priorizing nodes with the lowest latency.
// When there are errors, we start new requests to compensate. // When there are errors, we start new requests to compensate.
// Reorder requests to priorize closeness / low latency // Retrieve some status variables that we will use to sort requests
let request_order = self.request_order(to); let peer_list = self.0.fullmesh.get_peer_list();
let mut ord_requests = vec![(); request_order.len()] let ring: Arc<Ring> = self.0.ring.borrow().clone();
.into_iter() let our_zone = match ring.layout.node_role(&self.0.our_node_id) {
.map(|_| None) 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,
)
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for (to, fut) in requests {
let i = request_order.iter().position(|x| *x == to).unwrap(); // Sort requests by (priorize ourself, priorize same zone, priorize low latency)
ord_requests[i] = Some((to, fut)); requests
} .sort_by_key(|(diffnode, diffzone, ping, _to, _fut)| (*diffnode, *diffzone, *ping));
// Make an iterator to take requests in their sorted order // Make an iterator to take requests in their sorted order
let mut requests = ord_requests.into_iter().map(Option::unwrap); let mut requests = requests.into_iter();
// resp_stream will contain all of the requests that are currently in flight. // 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) // (for the moment none, they will be added in the loop below)
@@ -316,7 +350,7 @@ impl RpcHelper {
// If the current set of requests that are running is not enough to possibly // If the current set of requests that are running is not enough to possibly
// reach quorum, start some new requests. // reach quorum, start some new requests.
while successes.len() + resp_stream.len() < quorum { 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 tracer = opentelemetry::global::tracer("garage");
let span = tracer.start(format!("RPC to {:?}", req_to)); let span = tracer.start(format!("RPC to {:?}", req_to));
resp_stream.push(tokio::spawn( resp_stream.push(tokio::spawn(
@@ -386,49 +420,4 @@ impl RpcHelper {
Err(Error::Quorum(quorum, successes.len(), to.len(), errors)) Err(Error::Quorum(quorum, successes.len(), to.len(), errors))
} }
} }
pub fn request_order(&self, nodes: &[Uuid]) -> Vec<Uuid> {
// Retrieve some status variables that we will use to sort requests
let peer_list = self.0.fullmesh.get_peer_list();
let ring: Arc<Ring> = 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::<Vec<_>>();
// 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::<Vec<_>>()
}
} }
+21 -36
View File
@@ -16,8 +16,8 @@ use tokio::sync::watch;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use netapp::endpoint::{Endpoint, EndpointHandler}; use netapp::endpoint::{Endpoint, EndpointHandler};
use netapp::message::*;
use netapp::peering::fullmesh::FullMeshPeeringStrategy; use netapp::peering::fullmesh::FullMeshPeeringStrategy;
use netapp::proto::*;
use netapp::util::parse_and_resolve_peer_addr; use netapp::util::parse_and_resolve_peer_addr;
use netapp::{NetApp, NetworkKey, NodeID, NodeKey}; use netapp::{NetApp, NetworkKey, NodeID, NodeKey};
@@ -37,12 +37,10 @@ use crate::rpc_helper::*;
const DISCOVERY_INTERVAL: Duration = Duration::from_secs(60); const DISCOVERY_INTERVAL: Duration = Duration::from_secs(60);
const STATUS_EXCHANGE_INTERVAL: Duration = Duration::from_secs(10); const STATUS_EXCHANGE_INTERVAL: Duration = Duration::from_secs(10);
const SYSTEM_RPC_TIMEOUT: Duration = Duration::from_secs(15); const PING_TIMEOUT: Duration = Duration::from_secs(2);
/// Version tag used for version check upon Netapp connection. /// Version tag used for version check upon Netapp connection
/// Cluster nodes with different version tags are deemed pub const GARAGE_VERSION_TAG: u64 = 0x6761726167650007; // garage 0x0007
/// incompatible and will refuse to connect.
pub const GARAGE_VERSION_TAG: u64 = 0x6761726167650008; // garage 0x0008
/// RPC endpoint used for calls related to membership /// RPC endpoint used for calls related to membership
pub const SYSTEM_RPC_PATH: &str = "garage_rpc/membership.rs/SystemRpc"; pub const SYSTEM_RPC_PATH: &str = "garage_rpc/membership.rs/SystemRpc";
@@ -198,7 +196,7 @@ impl System {
background: Arc<BackgroundRunner>, background: Arc<BackgroundRunner>,
replication_factor: usize, replication_factor: usize,
config: &Config, config: &Config,
) -> Result<Arc<Self>, Error> { ) -> Arc<Self> {
let node_key = let node_key =
gen_node_key(&config.metadata_dir).expect("Unable to read or generate node ID"); gen_node_key(&config.metadata_dir).expect("Unable to read or generate node ID");
info!( info!(
@@ -206,21 +204,11 @@ impl System {
hex::encode(&node_key.public_key()[..8]) hex::encode(&node_key.public_key()[..8])
); );
let persist_cluster_layout: Persister<ClusterLayout> = let persist_cluster_layout = Persister::new(&config.metadata_dir, "cluster_layout");
Persister::new(&config.metadata_dir, "cluster_layout");
let persist_peer_list = Persister::new(&config.metadata_dir, "peer_list"); let persist_peer_list = Persister::new(&config.metadata_dir, "peer_list");
let cluster_layout = match persist_cluster_layout.load() { let cluster_layout = match persist_cluster_layout.load() {
Ok(x) => { Ok(x) => 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) => { Err(e) => {
info!( info!(
"No valid previous cluster layout stored ({}), starting fresh.", "No valid previous cluster layout stored ({}), starting fresh.",
@@ -313,7 +301,7 @@ impl System {
metadata_dir: config.metadata_dir.clone(), metadata_dir: config.metadata_dir.clone(),
}); });
sys.system_endpoint.set_handler(sys.clone()); sys.system_endpoint.set_handler(sys.clone());
Ok(sys) sys
} }
/// Perform bootstraping, starting the ping loop /// Perform bootstraping, starting the ping loop
@@ -331,6 +319,14 @@ impl System {
// ---- Administrative operations (directly available and // ---- Administrative operations (directly available and
// also available through RPC) ---- // 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<KnownNodeInfo> { pub fn get_known_nodes(&self) -> Vec<KnownNodeInfo> {
let node_status = self.node_status.read().unwrap(); let node_status = self.node_status.read().unwrap();
let known_nodes = self let known_nodes = self
@@ -495,7 +491,7 @@ impl System {
let local_info = self.local_status.load(); let local_info = self.local_status.load();
if local_info.replication_factor < info.replication_factor { if local_info.replication_factor < info.replication_factor {
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.", error!("Some node have a higher replication factor ({}) than this one ({}). This is not supported and might lead to bugs",
info.replication_factor, info.replication_factor,
local_info.replication_factor); local_info.replication_factor);
std::process::exit(1); std::process::exit(1);
@@ -523,16 +519,6 @@ impl System {
self: &Arc<Self>, self: &Arc<Self>,
adv: &ClusterLayout, adv: &ClusterLayout,
) -> Result<SystemRpc, Error> { ) -> Result<SystemRpc, Error> {
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 update_ring = self.update_ring.lock().await;
let mut layout: ClusterLayout = self.ring.borrow().layout.clone(); let mut layout: ClusterLayout = self.ring.borrow().layout.clone();
@@ -558,7 +544,7 @@ impl System {
SystemRpc::AdvertiseClusterLayout(layout), SystemRpc::AdvertiseClusterLayout(layout),
RequestStrategy::with_priority(PRIO_HIGH), RequestStrategy::with_priority(PRIO_HIGH),
) )
.await?; .await;
Ok(()) Ok(())
}); });
self.background.spawn(self.clone().save_cluster_layout()); self.background.spawn(self.clone().save_cluster_layout());
@@ -573,12 +559,11 @@ impl System {
self.update_local_status(); self.update_local_status();
let local_status: NodeStatus = self.local_status.load().as_ref().clone(); let local_status: NodeStatus = self.local_status.load().as_ref().clone();
let _ = self self.rpc
.rpc
.broadcast( .broadcast(
&self.system_endpoint, &self.system_endpoint,
SystemRpc::AdvertiseStatus(local_status), SystemRpc::AdvertiseStatus(local_status),
RequestStrategy::with_priority(PRIO_HIGH).with_timeout(SYSTEM_RPC_TIMEOUT), RequestStrategy::with_priority(PRIO_HIGH).with_timeout(PING_TIMEOUT),
) )
.await; .await;
@@ -702,7 +687,7 @@ impl System {
&self.system_endpoint, &self.system_endpoint,
peer, peer,
SystemRpc::PullClusterLayout, SystemRpc::PullClusterLayout,
RequestStrategy::with_priority(PRIO_HIGH).with_timeout(SYSTEM_RPC_TIMEOUT), RequestStrategy::with_priority(PRIO_HIGH).with_timeout(PING_TIMEOUT),
) )
.await; .await;
if let Ok(SystemRpc::AdvertiseClusterLayout(layout)) = resp { if let Ok(SystemRpc::AdvertiseClusterLayout(layout)) = resp {
+3 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_table" name = "garage_table"
version = "0.8.0" version = "0.7.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
@@ -15,8 +15,8 @@ path = "lib.rs"
[dependencies] [dependencies]
garage_db = { version = "0.8.0", path = "../db" } garage_db = { version = "0.8.0", path = "../db" }
garage_rpc = { version = "0.8.0", path = "../rpc" } garage_rpc = { version = "0.7.0", path = "../rpc" }
garage_util = { version = "0.8.0", path = "../util" } garage_util = { version = "0.7.0", path = "../util" }
opentelemetry = "0.17" opentelemetry = "0.17"
+1 -2
View File
@@ -25,8 +25,7 @@ use crate::replication::*;
use crate::schema::*; use crate::schema::*;
const TABLE_GC_BATCH_SIZE: usize = 1024; 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(30);
const TABLE_GC_RPC_TIMEOUT: Duration = Duration::from_secs(15);
// GC delay for table entries: 1 day (24 hours) // GC delay for table entries: 1 day (24 hours)
// (the delay before the entry is added in the GC todo list // (the delay before the entry is added in the GC todo list
+1 -1
View File
@@ -60,7 +60,7 @@ pub trait Entry<P: PartitionKey, S: SortKey>:
} }
/// Trait for the schema used in a table /// Trait for the schema used in a table
pub trait TableSchema: Send + Sync + 'static { pub trait TableSchema: Send + Sync {
/// The name of the table in the database /// The name of the table in the database
const TABLE_NAME: &'static str; const TABLE_NAME: &'static str;
+1 -2
View File
@@ -24,8 +24,7 @@ use crate::merkle::*;
use crate::replication::*; use crate::replication::*;
use crate::*; use crate::*;
// Sync RPC can contain a lot of data, so have a 1min timeout const TABLE_SYNC_RPC_TIMEOUT: Duration = Duration::from_secs(30);
const TABLE_SYNC_RPC_TIMEOUT: Duration = Duration::from_secs(60);
// Do anti-entropy every 10 minutes // Do anti-entropy every 10 minutes
const ANTI_ENTROPY_INTERVAL: Duration = Duration::from_secs(10 * 60); const ANTI_ENTROPY_INTERVAL: Duration = Duration::from_secs(10 * 60);
+2 -1
View File
@@ -31,7 +31,7 @@ use crate::schema::*;
use crate::sync::*; use crate::sync::*;
use crate::util::*; use crate::util::*;
pub const TABLE_RPC_TIMEOUT: Duration = Duration::from_secs(30); pub const TABLE_RPC_TIMEOUT: Duration = Duration::from_secs(10);
pub struct Table<F: TableSchema + 'static, R: TableReplication + 'static> { pub struct Table<F: TableSchema + 'static, R: TableReplication + 'static> {
pub system: Arc<System>, pub system: Arc<System>,
@@ -113,6 +113,7 @@ where
async fn insert_internal(&self, e: &F::E) -> Result<(), Error> { async fn insert_internal(&self, e: &F::E) -> Result<(), Error> {
let hash = e.partition_key().hash(); let hash = e.partition_key().hash();
let who = self.data.replication.write_nodes(&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 e_enc = Arc::new(ByteBuf::from(rmp_to_vec_all_named(e)?));
let rpc = TableRpc::<F>::Update(vec![e_enc]); let rpc = TableRpc::<F>::Update(vec![e_enc]);
+5 -9
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_util" name = "garage_util"
version = "0.8.0" version = "0.7.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
@@ -16,19 +16,14 @@ path = "lib.rs"
[dependencies] [dependencies]
garage_db = { version = "0.8.0", path = "../db" } garage_db = { version = "0.8.0", path = "../db" }
arc-swap = "1.0"
async-trait = "0.1" async-trait = "0.1"
blake2 = "0.9" blake2 = "0.9"
bytes = "1.0"
digest = "0.10"
err-derive = "0.3" err-derive = "0.3"
git-version = "0.3.4"
xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] } xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] }
hex = "0.4" hex = "0.4"
lazy_static = "1.4"
tracing = "0.1.30" tracing = "0.1.30"
rand = "0.8" rand = "0.8"
sha2 = "0.10" sha2 = "0.9"
chrono = "0.4" chrono = "0.4"
rmp-serde = "0.15" rmp-serde = "0.15"
@@ -39,13 +34,14 @@ toml = "0.5"
futures = "0.3" futures = "0.3"
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] } tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
netapp = "0.5" #netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
#netapp = { version = "0.4", path = "../../../netapp" }
netapp = "0.4"
http = "0.2" http = "0.2"
hyper = "0.14" hyper = "0.14"
opentelemetry = { version = "0.17", features = [ "rt-tokio", "metrics", "trace" ] } opentelemetry = { version = "0.17", features = [ "rt-tokio", "metrics", "trace" ] }
[features] [features]
k2v = [] k2v = []
-61
View File
@@ -1,61 +0,0 @@
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<D: Digest> {
sendblk: mpsc::Sender<Bytes>,
task: JoinHandle<digest::Output<D>>,
}
impl<D: Digest> AsyncHasher<D> {
pub fn new() -> Self {
let (sendblk, mut recvblk) = mpsc::channel::<Bytes>(1);
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 async fn update(&self, b: Bytes) {
self.sendblk.send(b).await.unwrap();
}
pub async fn finalize(self) -> digest::Output<D> {
drop(self.sendblk);
self.task.await.unwrap()
}
}
impl<D: Digest> Default for AsyncHasher<D> {
fn default() -> Self {
Self::new()
}
}
+11 -2
View File
@@ -23,6 +23,10 @@ pub struct Config {
#[serde(default = "default_block_size")] #[serde(default = "default_block_size")]
pub block_size: usize, 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: /// Replication mode. Supported values:
/// - none, 1 -> no replication /// - none, 1 -> no replication
/// - 2 -> 2-way replication /// - 2 -> 2-way replication
@@ -77,10 +81,11 @@ pub struct Config {
pub s3_api: S3ApiConfig, pub s3_api: S3ApiConfig,
/// Configuration for K2V api /// Configuration for K2V api
#[cfg(feature = "k2v")]
pub k2v_api: Option<K2VApiConfig>, pub k2v_api: Option<K2VApiConfig>,
/// Configuration for serving files as normal web server /// Configuration for serving files as normal web server
pub s3_web: Option<WebConfig>, pub s3_web: WebConfig,
/// Configuration for the admin API endpoint /// Configuration for the admin API endpoint
#[serde(default = "Default::default")] #[serde(default = "Default::default")]
@@ -91,7 +96,7 @@ pub struct Config {
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct S3ApiConfig { pub struct S3ApiConfig {
/// Address and port to bind for api serving /// Address and port to bind for api serving
pub api_bind_addr: Option<SocketAddr>, pub api_bind_addr: SocketAddr,
/// S3 region to use /// S3 region to use
pub s3_region: String, pub s3_region: String,
/// Suffix to remove from domain name to find bucket. If None, /// Suffix to remove from domain name to find bucket. If None,
@@ -100,6 +105,7 @@ pub struct S3ApiConfig {
} }
/// Configuration for K2V api /// Configuration for K2V api
#[cfg(feature = "k2v")]
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct K2VApiConfig { pub struct K2VApiConfig {
/// Address and port to bind for api serving /// Address and port to bind for api serving
@@ -141,6 +147,9 @@ fn default_sled_flush_every_ms() -> u64 {
fn default_block_size() -> usize { fn default_block_size() -> usize {
1048576 1048576
} }
fn default_block_manager_background_tranquility() -> u32 {
2
}
/// Read and parse configuration /// Read and parse configuration
pub fn read_config(config_file: PathBuf) -> Result<Config, Error> { pub fn read_config(config_file: PathBuf) -> Result<Config, Error> {
+1 -1
View File
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use crate::crdt::crdt::*; use crate::crdt::crdt::*;
/// Boolean, where `true` is an absorbing state /// Boolean, where `true` is an absorbing state
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
pub struct Bool(bool); pub struct Bool(bool);
impl Bool { impl Bool {
+1 -1
View File
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use crate::crdt::crdt::*; use crate::crdt::crdt::*;
/// Deletable object (once deleted, cannot go back) /// Deletable object (once deleted, cannot go back)
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
pub enum Deletable<T> { pub enum Deletable<T> {
Present(T), Present(T),
Deleted, Deleted,
+1 -1
View File
@@ -37,7 +37,7 @@ use crate::crdt::crdt::*;
/// ///
/// This scheme is used by AWS S3 or Soundcloud and often without knowing /// This scheme is used by AWS S3 or Soundcloud and often without knowing
/// in enterprise when reconciliating databases with ad-hoc scripts. /// in enterprise when reconciliating databases with ad-hoc scripts.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Lww<T> { pub struct Lww<T> {
ts: u64, ts: u64,
v: T, v: T,
+1 -1
View File
@@ -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`, /// 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 /// the serialization cost `O(n)` would still have to be paid at each modification, so we are
/// actually not losing anything here. /// actually not losing anything here.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct LwwMap<K, V> { pub struct LwwMap<K, V> {
vals: Vec<(K, u64, V)>, vals: Vec<(K, u64, V)>,
} }
+1 -1
View File
@@ -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`, /// 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 /// the serialization cost `O(n)` would still have to be paid at each modification, so we are
/// actually not losing anything here. /// actually not losing anything here.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Map<K, V> { pub struct Map<K, V> {
vals: Vec<(K, V)>, vals: Vec<(K, V)>,
} }
-2
View File
@@ -3,7 +3,6 @@
#[macro_use] #[macro_use]
extern crate tracing; extern crate tracing;
pub mod async_hash;
pub mod background; pub mod background;
pub mod config; pub mod config;
pub mod crdt; pub mod crdt;
@@ -15,4 +14,3 @@ pub mod persister;
pub mod time; pub mod time;
pub mod token_bucket; pub mod token_bucket;
pub mod tranquilizer; pub mod tranquilizer;
pub mod version;
-28
View File
@@ -1,28 +0,0 @@
use std::sync::Arc;
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 {
&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)));
}
+5 -5
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_web" name = "garage_web"
version = "0.8.0" version = "0.7.0"
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"] authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" 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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
garage_api = { version = "0.8.0", path = "../api" } garage_api = { version = "0.7.0", path = "../api" }
garage_model = { version = "0.8.0", path = "../model" } garage_model = { version = "0.7.0", path = "../model" }
garage_util = { version = "0.8.0", path = "../util" } garage_util = { version = "0.7.0", path = "../util" }
garage_table = { version = "0.8.0", path = "../table" } garage_table = { version = "0.7.0", path = "../table" }
err-derive = "0.3" err-derive = "0.3"
tracing = "0.1.30" tracing = "0.1.30"
+1 -1
View File
@@ -6,4 +6,4 @@ mod error;
pub use error::Error; pub use error::Error;
mod web_server; mod web_server;
pub use web_server::WebServer; pub use web_server::run_web_server;
+198 -211
View File
@@ -57,226 +57,90 @@ impl WebMetrics {
} }
} }
pub struct WebServer { /// Run a web server
pub async fn run_web_server(
garage: Arc<Garage>, garage: Arc<Garage>,
metrics: Arc<WebMetrics>, shutdown_signal: impl Future<Output = ()>,
root_domain: String, ) -> 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<Body>| {
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(())
} }
impl WebServer { async fn handle_request(
/// Run a web server garage: Arc<Garage>,
pub async fn run( metrics: Arc<WebMetrics>,
garage: Arc<Garage>, req: Request<Body>,
addr: SocketAddr, addr: SocketAddr,
root_domain: String, ) -> Result<Response<Body>, Infallible> {
shutdown_signal: impl Future<Output = ()>, info!("{} {} {}", addr, req.method(), req.uri());
) -> Result<(), GarageError> {
let metrics = Arc::new(WebMetrics::new());
let web_server = Arc::new(WebServer {
garage,
metrics,
root_domain,
});
let service = make_service_fn(|conn: &AddrStream| { // Lots of instrumentation
let web_server = web_server.clone(); 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 client_addr = conn.remote_addr(); let metrics_tags = &[KeyValue::new("method", req.method().to_string())];
async move {
Ok::<_, Error>(service_fn(move |req: Request<Body>| {
let web_server = web_server.clone();
web_server.handle_request(req, client_addr) // 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 server = Server::bind(&addr).serve(service); // More instrumentation
let graceful = server.with_graceful_shutdown(shutdown_signal); metrics.request_counter.add(1, &metrics_tags[..]);
info!("Web server listening on http://{}", addr);
graceful.await?; // Returning the result
Ok(()) match res {
} Ok(res) => {
debug!("{} {} {}", req.method(), res.status(), req.uri());
async fn handle_request( Ok(res)
self: Arc<Self>,
req: Request<Body>,
addr: SocketAddr,
) -> Result<Response<Body>, 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!(
async fn serve_file(self: &Arc<Self>, req: &Request<Body>) -> Result<Response<Body>, Error> { "{} {} {} {}",
// Get http authority string (eg. [::1]:3902 or garage.tld:80) req.method(),
let authority = req error.http_status_code(),
.headers() req.uri(),
.get(HOST) error
.ok_or_bad_request("HOST header required")? );
.to_str()?; metrics.error_counter.add(
1,
// Get bucket &[
let host = authority_to_host(authority)?; metrics_tags[0].clone(),
KeyValue::new("status_code", error.http_status_code().to_string()),
let bucket_name = host_to_bucket(&host, &self.root_domain).unwrap_or(&host); ],
let bucket_id = self );
.garage Ok(error_to_res(error))
.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)
}
} }
} }
} }
@@ -296,6 +160,129 @@ fn error_to_res(e: Error) -> Response<Body> {
http_error http_error
} }
async fn serve_file(garage: Arc<Garage>, req: &Request<Body>) -> Result<Response<Body>, 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 /// Path to key
/// ///
/// Convert the provided path to the internal key /// Convert the provided path to the internal key