mirror of
https://github.com/deuxfleurs-org/garage.git
synced 2026-08-14 16:13:11 +00:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 08b1e8a7ea | |||
| ad7ab31411 | |||
| 74a7a550eb | |||
| cc255d46cd | |||
| 8e25a37f0e | |||
| e342db19aa | |||
| f3405b6378 | |||
| 860ccf2811 | |||
| 9df7559446 | |||
| a97467075d | |||
| 9d7535c3f5 | |||
| da6efb4b23 | |||
| e8811f7c9d | |||
| 2090a6187f | |||
| 6f13d083ab | |||
| 8c4f418fe8 | |||
| bef6d627b0 | |||
| e93d7fb228 | |||
| eaf54efb25 | |||
| 93f8d59e4c | |||
| cc1caa87fb | |||
| 69b89fb46d | |||
| 6b47c294f5 | |||
| 28c015d9ff | |||
| 4e8af1d956 | |||
| 3e7f766d95 | |||
| 43e13a501d | |||
| ada7899b24 | |||
| b2c51844a1 | |||
| f6ebcbc7a7 | |||
| df8a4068d9 | |||
| de4276202a | |||
| 1b450c4b49 | |||
| 4067797d01 |
+1
-117
@@ -36,7 +36,7 @@ steps:
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- nix-shell --arg release false --run "cargo fmt -- --check"
|
||||
- nix-shell --arg release false --run "cargo clippy -- --allow clippy::needless_borrow --allow clippy::needless-return --deny warnings"
|
||||
- nix-shell --arg release false --run "cargo clippy -- --deny warnings"
|
||||
|
||||
- name: build
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
@@ -80,38 +80,6 @@ steps:
|
||||
- nix-build --no-build-output --argstr target x86_64-unknown-linux-musl --arg release false --argstr git_version $DRONE_COMMIT
|
||||
- nix-shell --arg release false --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||
|
||||
- name: update cache
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: cache_aws_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: cache_aws_secret_access_key
|
||||
NIX_PRIV_KEY:
|
||||
from_secret: nix_priv_key
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- (umask 377 && echo $NIX_PRIV_KEY > /etc/nix/signing-key.sec)
|
||||
- |
|
||||
nix copy --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/etc/nix/signing-key.sec' \
|
||||
$(nix-store -qR --include-outputs \
|
||||
$(nix-build --no-out-link shell.nix --arg release false -A inputDerivation))
|
||||
- |
|
||||
nix copy --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/etc/nix/signing-key.sec' \
|
||||
$(nix-store -qR --include-outputs \
|
||||
$(nix-instantiate --argstr target x86_64-unknown-linux-musl --argstr compileMode test))
|
||||
- |
|
||||
nix copy --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/etc/nix/signing-key.sec' \
|
||||
$(nix-store -qR --include-outputs \
|
||||
$(nix-instantiate --argstr target x86_64-unknown-linux-musl --arg release false))
|
||||
when:
|
||||
event:
|
||||
- cron
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- custom
|
||||
@@ -212,27 +180,6 @@ steps:
|
||||
commands:
|
||||
- nix-shell --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||
|
||||
- name: update cache
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: cache_aws_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: cache_aws_secret_access_key
|
||||
NIX_PRIV_KEY:
|
||||
from_secret: nix_priv_key
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- (umask 377 && echo $NIX_PRIV_KEY > /etc/nix/signing-key.sec)
|
||||
- |
|
||||
nix copy --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/etc/nix/signing-key.sec' \
|
||||
$(nix-store -qR --include-outputs \
|
||||
$(nix-instantiate --argstr target $TARGET --arg release true))
|
||||
|
||||
- name: push static binary
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
@@ -323,27 +270,6 @@ steps:
|
||||
commands:
|
||||
- nix-shell --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||
|
||||
- name: update cache
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: cache_aws_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: cache_aws_secret_access_key
|
||||
NIX_PRIV_KEY:
|
||||
from_secret: nix_priv_key
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- (umask 377 && echo $NIX_PRIV_KEY > /etc/nix/signing-key.sec)
|
||||
- |
|
||||
nix copy --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/etc/nix/signing-key.sec' \
|
||||
$(nix-store -qR --include-outputs \
|
||||
$(nix-instantiate --argstr target $TARGET --arg release true))
|
||||
|
||||
- name: push static binary
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
@@ -423,27 +349,6 @@ steps:
|
||||
commands:
|
||||
- nix-build --no-build-output --argstr target $TARGET --arg release true --argstr git_version $DRONE_COMMIT
|
||||
|
||||
- name: update cache
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: cache_aws_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: cache_aws_secret_access_key
|
||||
NIX_PRIV_KEY:
|
||||
from_secret: nix_priv_key
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- (umask 377 && echo $NIX_PRIV_KEY > /etc/nix/signing-key.sec)
|
||||
- |
|
||||
nix copy --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/etc/nix/signing-key.sec' \
|
||||
$(nix-store -qR --include-outputs \
|
||||
$(nix-instantiate --argstr target $TARGET --arg release true))
|
||||
|
||||
- name: push static binary
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
@@ -523,27 +428,6 @@ steps:
|
||||
commands:
|
||||
- nix-build --no-build-output --argstr target $TARGET --arg release true --argstr git_version $DRONE_COMMIT
|
||||
|
||||
- name: update cache
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: cache_aws_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: cache_aws_secret_access_key
|
||||
NIX_PRIV_KEY:
|
||||
from_secret: nix_priv_key
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- (umask 377 && echo $NIX_PRIV_KEY > /etc/nix/signing-key.sec)
|
||||
- |
|
||||
nix copy --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/etc/nix/signing-key.sec' \
|
||||
$(nix-store -qR --include-outputs \
|
||||
$(nix-instantiate --argstr target $TARGET --arg release true))
|
||||
|
||||
- name: push static binary
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
|
||||
Generated
+2
-1
@@ -874,7 +874,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "netapp"
|
||||
version = "0.3.0"
|
||||
source = "git+https://git.deuxfleurs.fr/lx/netapp#57327f10e2536a89004f3a1def83ed16243c1a3e"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d202ff23744499a8e5d89a3199ca7b9a8aef9980a23b1b09d6508a8db2a118d2"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-trait",
|
||||
|
||||
@@ -246,7 +246,7 @@ in
|
||||
registry = "registry+https://github.com/rust-lang/crates.io-index";
|
||||
src = fetchCratesIo { inherit name version; sha256 = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"; };
|
||||
dependencies = {
|
||||
${ if hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" || hostPlatform.config == "aarch64-apple-darwin" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.103" { inherit profileName; };
|
||||
${ if hostPlatform.config == "aarch64-apple-darwin" || hostPlatform.parsed.cpu.name == "aarch64" && hostPlatform.parsed.kernel.name == "linux" then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.103" { inherit profileName; };
|
||||
};
|
||||
});
|
||||
|
||||
@@ -626,7 +626,7 @@ in
|
||||
hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; };
|
||||
sodiumoxide = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; };
|
||||
log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.14" { inherit profileName; };
|
||||
netapp = rustPackages."git+https://git.deuxfleurs.fr/lx/netapp".netapp."0.3.0" { inherit profileName; };
|
||||
netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.3.0" { inherit profileName; };
|
||||
pretty_env_logger = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pretty_env_logger."0.4.0" { inherit profileName; };
|
||||
rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.4" { inherit profileName; };
|
||||
rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; };
|
||||
@@ -687,7 +687,7 @@ in
|
||||
garage_util = rustPackages."unknown".garage_util."0.4.0" { inherit profileName; };
|
||||
hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; };
|
||||
log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.14" { inherit profileName; };
|
||||
netapp = rustPackages."git+https://git.deuxfleurs.fr/lx/netapp".netapp."0.3.0" { inherit profileName; };
|
||||
netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.3.0" { inherit profileName; };
|
||||
rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.4" { inherit profileName; };
|
||||
rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; };
|
||||
serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.130" { inherit profileName; };
|
||||
@@ -714,7 +714,7 @@ in
|
||||
hyper = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.13" { inherit profileName; };
|
||||
sodiumoxide = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; };
|
||||
log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.14" { inherit profileName; };
|
||||
netapp = rustPackages."git+https://git.deuxfleurs.fr/lx/netapp".netapp."0.3.0" { inherit profileName; };
|
||||
netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.3.0" { inherit profileName; };
|
||||
rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.4" { inherit profileName; };
|
||||
rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; };
|
||||
serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.130" { inherit profileName; };
|
||||
@@ -761,7 +761,7 @@ in
|
||||
http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.5" { inherit profileName; };
|
||||
hyper = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.13" { inherit profileName; };
|
||||
log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.14" { inherit profileName; };
|
||||
netapp = rustPackages."git+https://git.deuxfleurs.fr/lx/netapp".netapp."0.3.0" { inherit profileName; };
|
||||
netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.3.0" { inherit profileName; };
|
||||
rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.4" { inherit profileName; };
|
||||
rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; };
|
||||
serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.130" { inherit profileName; };
|
||||
@@ -1228,15 +1228,11 @@ in
|
||||
};
|
||||
});
|
||||
|
||||
"git+https://git.deuxfleurs.fr/lx/netapp".netapp."0.3.0" = overridableMkRustCrate (profileName: rec {
|
||||
"registry+https://github.com/rust-lang/crates.io-index".netapp."0.3.0" = overridableMkRustCrate (profileName: rec {
|
||||
name = "netapp";
|
||||
version = "0.3.0";
|
||||
registry = "git+https://git.deuxfleurs.fr/lx/netapp";
|
||||
src = fetchCrateGit {
|
||||
url = https://git.deuxfleurs.fr/lx/netapp;
|
||||
name = "netapp";
|
||||
version = "0.3.0";
|
||||
rev = "57327f10e2536a89004f3a1def83ed16243c1a3e";};
|
||||
registry = "registry+https://github.com/rust-lang/crates.io-index";
|
||||
src = fetchCratesIo { inherit name version; sha256 = "d202ff23744499a8e5d89a3199ca7b9a8aef9980a23b1b09d6508a8db2a118d2"; };
|
||||
features = builtins.concatLists [
|
||||
[ "default" ]
|
||||
];
|
||||
|
||||
+1
-1
@@ -4,4 +4,4 @@ ENV RUST_BACKTRACE=1
|
||||
ENV RUST_LOG=garage=info
|
||||
|
||||
COPY result/bin/garage /
|
||||
CMD [ "/garage", "server", "-c", "config.toml"]
|
||||
CMD [ "/garage", "server"]
|
||||
|
||||
+2
-11
@@ -11,26 +11,17 @@ with import ./nix/common.nix;
|
||||
let
|
||||
crossSystem = { config = target; };
|
||||
in let
|
||||
|
||||
pkgs = import pkgsSrc {
|
||||
inherit system crossSystem;
|
||||
overlays = [ cargo2nixOverlay rustOverlay ];
|
||||
overlays = [ cargo2nixOverlay ];
|
||||
};
|
||||
rustDist = pkgs.buildPackages.rust-bin.stable.latest.default;
|
||||
|
||||
/*
|
||||
The following complexity should be abstracted by makePackageSet' (note the final quote).
|
||||
However its code uses deprecated features of rust-overlay that can lead to bug.
|
||||
Instead, we build our own rustChannel object with the recommended API of rust-overlay.
|
||||
*/
|
||||
rustChannel = rustDist // {
|
||||
cargo = rustDist;
|
||||
rustc = rustDist.override {
|
||||
targets = [
|
||||
(pkgs.rustBuilder.rustLib.realHostTriple pkgs.stdenv.targetPlatform)
|
||||
];
|
||||
};
|
||||
};
|
||||
rustChannel = pkgs.rustPlatform.rust;
|
||||
|
||||
overrides = pkgs.buildPackages.rustBuilder.overrides.all ++ [
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
|
||||
+16
-7
@@ -5,14 +5,21 @@
|
||||
- [Quick start](./quick_start/index.md)
|
||||
|
||||
- [Cookbook](./cookbook/index.md)
|
||||
- [Deploying Garage](./cookbook/real_world.md)
|
||||
- [Configuring S3 clients](./cookbook/clients.md)
|
||||
- [Hosting a website](./cookbook/website.md)
|
||||
- [Recovering from failures](./cookbook/recovering.md)
|
||||
- [Building from source](./cookbook/from_source.md)
|
||||
- [Starting with Systemd](./cookbook/systemd.md)
|
||||
- [Integrate as a media backend]()
|
||||
- [Operate a cluster]()
|
||||
- [Integration with systemd](./cookbook/systemd.md)
|
||||
- [Gateways](./cookbook/gateways.md)
|
||||
- [Exposing buckets as websites](./cookbook/exposing_websites.md)
|
||||
- [Configuring a reverse proxy](./cookbook/reverse_proxy.md)
|
||||
- [Production Deployment](./cookbook/real_world.md)
|
||||
- [Recovering from failures](./cookbook/recovering.md)
|
||||
|
||||
- [Integrations](./connect/index.md)
|
||||
- [Apps (Nextcloud, Peertube...)](./connect/apps.md)
|
||||
- [Websites (Hugo, Jekyll, Publii...)](./connect/websites.md)
|
||||
- [Repositories (Docker, Nix...)](./connect/repositories.md)
|
||||
- [CLI tools (rclone, awscli, mc...)](./connect/cli.md)
|
||||
- [Your code (PHP, JS, Go...)](./connect/code.md)
|
||||
|
||||
|
||||
- [Reference Manual](./reference_manual/index.md)
|
||||
- [Garage configuration file](./reference_manual/configuration.md)
|
||||
@@ -22,6 +29,7 @@
|
||||
- [Design](./design/index.md)
|
||||
- [Related Work](./design/related_work.md)
|
||||
- [Internals](./design/internals.md)
|
||||
- [Design draft](./design/design_draft.md)
|
||||
|
||||
- [Development](./development/index.md)
|
||||
- [Setup your environment](./development/devenv.md)
|
||||
@@ -30,3 +38,4 @@
|
||||
|
||||
- [Working Documents](./working_documents/index.md)
|
||||
- [Load Balancing Data](./working_documents/load_balancing.md)
|
||||
- [Migrating from 0.3 to 0.4](./working_documents/migration_04.md)
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
# Apps (Nextcloud, Peertube...)
|
||||
|
||||
In this section, we cover the following software: [Nextcloud](#nextcloud), [Peertube](#peertube), [Mastodon](#mastodon), [Matrix](#matrix)
|
||||
|
||||
## Nextcloud
|
||||
|
||||
Nextcloud is a popular file synchronisation and backup service.
|
||||
By default, Nextcloud stores its data on the local filesystem.
|
||||
If you want to expand your storage to aggregate multiple servers, Garage is the way to go.
|
||||
|
||||
A S3 backend can be configured in two ways on Nextcloud, either as Primary Storage or as an External Storage.
|
||||
Primary storage will store all your data on S3, in an opaque manner, and will provide the best performances.
|
||||
External storage enable you to select which data will be stored on S3, your file hierarchy will be preserved in S3, but it might be slower.
|
||||
|
||||
In the following, we cover both methods but before reading our guide, we suppose you have done some preliminary steps.
|
||||
First, we expect you have an already installed and configured Nextcloud instance.
|
||||
Second, we suppose you have created a key and a bucket.
|
||||
|
||||
As a reminder, you can create a key for your nextcloud instance as follow:
|
||||
|
||||
```bash
|
||||
garage key new --name nextcloud-key
|
||||
```
|
||||
|
||||
Keep the Key ID and the Secret key in a pad, they will be needed later.
|
||||
Then you can create a bucket and give read/write rights to your key on this bucket with:
|
||||
|
||||
```bash
|
||||
garage bucket create nextcloud
|
||||
garage bucket allow nextcloud --read --write --key nextcloud-key
|
||||
```
|
||||
|
||||
|
||||
### Primary Storage
|
||||
|
||||
Now edit your Nextcloud configuration file to enable object storage.
|
||||
On my installation, the config. file is located at the following path: `/var/www/nextcloud/config/config.php`.
|
||||
We will add a new root key to the `$CONFIG` dictionnary named `objectstore`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$CONFIG = array(
|
||||
/* your existing configuration */
|
||||
'objectstore' => [
|
||||
'class' => '\\OC\\Files\\ObjectStore\\S3',
|
||||
'arguments' => [
|
||||
'bucket' => 'nextcloud', // Your bucket name, must be created before
|
||||
'autocreate' => false, // Garage does not support autocreate
|
||||
'key' => 'xxxxxxxxx', // The Key ID generated previously
|
||||
'secret' => 'xxxxxxxxx', // The Secret key generated previously
|
||||
'hostname' => '127.0.0.1', // Can also be a domain name, eg. garage.example.com
|
||||
'port' => 3900, // Put your reverse proxy port or your S3 API port
|
||||
'use_ssl' => false, // Set it to true if you have a TLS enabled reverse proxy
|
||||
'region' => 'garage', // Garage has only one region named "garage"
|
||||
'use_path_style' => true // Garage supports only path style, must be set to true
|
||||
],
|
||||
],
|
||||
```
|
||||
|
||||
That's all, your Nextcloud will store all your data to S3.
|
||||
To test your new configuration, just reload your Nextcloud webpage and start sending data.
|
||||
|
||||
*External link:* [Nextcloud Documentation > Primary Storage](https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/primary_storage.html)
|
||||
|
||||
### External Storage
|
||||
|
||||
**From the GUI.** Activate the "External storage support" app from the "Applications" page (click on your account icon on the top right corner of your screen to display the menu). Go to your parameters page (also located below your account icon). Click on external storage (or the corresponding translation in your language).
|
||||
|
||||
[](./cli-nextcloud-gui.png)
|
||||
*Click on the picture to zoom*
|
||||
|
||||
Add a new external storage. Put what you want in "folder name" (eg. "shared"). Select "Amazon S3". Keep "Access Key" for the Authentication field.
|
||||
In Configuration, put your bucket name (eg. nextcloud), the host (eg. 127.0.0.1), the port (eg. 3900 or 443), the region (garage). Tick the SSL box if you have put an HTTPS proxy in front of garage. You must tick the "Path access" box and you must leave the "Legacy authentication (v2)" box empty. Put your Key ID (eg. GK...) and your Secret Key in the last two input boxes. Finally click on the tick symbol on the right of your screen.
|
||||
|
||||
Now go to your "Files" app and a new "linked folder" has appeared with the name you chose earlier (eg. "shared").
|
||||
|
||||
*External link:* [Nextcloud Documentation > External Storage Configuration GUI](https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/external_storage_configuration_gui.html)
|
||||
|
||||
**From the CLI.** First install the external storage application:
|
||||
|
||||
```bash
|
||||
php occ app:install files_external
|
||||
```
|
||||
|
||||
Then add a new mount point with:
|
||||
|
||||
```bash
|
||||
php occ files_external:create \
|
||||
-c bucket=nextcloud \
|
||||
-c hostname=127.0.0.1 \
|
||||
-c port=3900 \
|
||||
-c region=garage \
|
||||
-c use_ssl=false \
|
||||
-c use_path_style=true \
|
||||
-c legacy_auth=false \
|
||||
-c key=GKxxxx \
|
||||
-c secret=xxxx \
|
||||
shared amazons3 amazons3::accesskey
|
||||
```
|
||||
|
||||
Adapt the `hostname`, `port`, `use_ssl`, `key`, and `secret` entries to your configuration.
|
||||
Do not change the `use_path_style` and `legacy_auth` entries, other configurations are not supported.
|
||||
|
||||
*External link:* [Nextcloud Documentation > occ command > files external](https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/occ_command.html#files-external-label)
|
||||
|
||||
|
||||
## Peertube
|
||||
|
||||
Peertube proposes a clever integration of S3 by directly exposing its endpoint instead of proxifying requests through the application.
|
||||
In other words, Peertube is only responsible of the "control plane" and offload the "data plane" to Garage.
|
||||
In return, this system is a bit harder to configure, especially with Garage that supports less feature than other older S3 backends.
|
||||
We show that it is still possible to configure Garage with Peertube, allowing you to spread the load and the bandiwdth usage on the Garage cluster.
|
||||
|
||||
### Enable path-style access by patching Peertube
|
||||
|
||||
First, you will need to apply a small patch on Peertube ([#4510](https://github.com/Chocobozzz/PeerTube/pull/4510)):
|
||||
|
||||
```diff
|
||||
From e3b4c641bdf67e07d406a1d49d6aa6b1fbce2ab4 Mon Sep 17 00:00:00 2001
|
||||
From: Martin Honermeyer <maze@strahlungsfrei.de>
|
||||
Date: Sun, 31 Oct 2021 12:34:04 +0100
|
||||
Subject: [PATCH] Allow setting path-style access for object storage
|
||||
|
||||
---
|
||||
config/default.yaml | 4 ++++
|
||||
config/production.yaml.example | 4 ++++
|
||||
server/initializers/config.ts | 1 +
|
||||
server/lib/object-storage/shared/client.ts | 3 ++-
|
||||
.../production/config/custom-environment-variables.yaml | 2 ++
|
||||
5 files changed, 13 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/config/default.yaml b/config/default.yaml
|
||||
index cf9d69a6211..4efd56fb804 100644
|
||||
--- a/config/default.yaml
|
||||
+++ b/config/default.yaml
|
||||
@@ -123,6 +123,10 @@ object_storage:
|
||||
# You can also use AWS_SECRET_ACCESS_KEY env variable
|
||||
secret_access_key: ''
|
||||
|
||||
+ # Reference buckets via path rather than subdomain
|
||||
+ # (i.e. "my-endpoint.com/bucket" instead of "bucket.my-endpoint.com")
|
||||
+ force_path_style: false
|
||||
+
|
||||
# Maximum amount to upload in one request to object storage
|
||||
max_upload_part: 2GB
|
||||
|
||||
diff --git a/config/production.yaml.example b/config/production.yaml.example
|
||||
index 70993bf57a3..9ca2de5f4c9 100644
|
||||
--- a/config/production.yaml.example
|
||||
+++ b/config/production.yaml.example
|
||||
@@ -121,6 +121,10 @@ object_storage:
|
||||
# You can also use AWS_SECRET_ACCESS_KEY env variable
|
||||
secret_access_key: ''
|
||||
|
||||
+ # Reference buckets via path rather than subdomain
|
||||
+ # (i.e. "my-endpoint.com/bucket" instead of "bucket.my-endpoint.com")
|
||||
+ force_path_style: false
|
||||
+
|
||||
# Maximum amount to upload in one request to object storage
|
||||
max_upload_part: 2GB
|
||||
|
||||
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
|
||||
index 8375bf4304c..d726c59a4b6 100644
|
||||
--- a/server/initializers/config.ts
|
||||
+++ b/server/initializers/config.ts
|
||||
@@ -91,6 +91,7 @@ const CONFIG = {
|
||||
ACCESS_KEY_ID: config.get<string>('object_storage.credentials.access_key_id'),
|
||||
SECRET_ACCESS_KEY: config.get<string>('object_storage.credentials.secret_access_key')
|
||||
},
|
||||
+ FORCE_PATH_STYLE: config.get<boolean>('object_storage.force_path_style'),
|
||||
VIDEOS: {
|
||||
BUCKET_NAME: config.get<string>('object_storage.videos.bucket_name'),
|
||||
PREFIX: config.get<string>('object_storage.videos.prefix'),
|
||||
diff --git a/server/lib/object-storage/shared/client.ts b/server/lib/object-storage/shared/client.ts
|
||||
index c9a61459336..eadad02f93f 100644
|
||||
--- a/server/lib/object-storage/shared/client.ts
|
||||
+++ b/server/lib/object-storage/shared/client.ts
|
||||
@@ -26,7 +26,8 @@ function getClient () {
|
||||
accessKeyId: OBJECT_STORAGE.CREDENTIALS.ACCESS_KEY_ID,
|
||||
secretAccessKey: OBJECT_STORAGE.CREDENTIALS.SECRET_ACCESS_KEY
|
||||
}
|
||||
- : undefined
|
||||
+ : undefined,
|
||||
+ forcePathStyle: CONFIG.OBJECT_STORAGE.FORCE_PATH_STYLE
|
||||
})
|
||||
|
||||
logger.info('Initialized S3 client %s with region %s.', getEndpoint(), OBJECT_STORAGE.REGION, lTags())
|
||||
diff --git a/support/docker/production/config/custom-environment-variables.yaml b/support/docker/production/config/custom-environment-variables.yaml
|
||||
index c7cd28e6521..a960bab0bc9 100644
|
||||
--- a/support/docker/production/config/custom-environment-variables.yaml
|
||||
+++ b/support/docker/production/config/custom-environment-variables.yaml
|
||||
@@ -54,6 +54,8 @@ object_storage:
|
||||
|
||||
region: "PEERTUBE_OBJECT_STORAGE_REGION"
|
||||
|
||||
+ force_path_style: "PEERTUBE_OBJECT_STORAGE_FORCE_PATH_STYLE"
|
||||
+
|
||||
max_upload_part:
|
||||
__name: "PEERTUBE_OBJECT_STORAGE_MAX_UPLOAD_PART"
|
||||
__format: "json"
|
||||
```
|
||||
|
||||
You can then recompile it with:
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
And it can be started with:
|
||||
|
||||
```
|
||||
NODE_ENV=production NODE_CONFIG_DIR=/srv/peertube/config node dist/server.js
|
||||
```
|
||||
|
||||
|
||||
### Create resources in Garage
|
||||
|
||||
Create a key for Peertube:
|
||||
|
||||
```bash
|
||||
garage key new --name peertube-key
|
||||
```
|
||||
|
||||
Keep the Key ID and the Secret key in a pad, they will be needed later.
|
||||
|
||||
We need two buckets, one for normal videos (named peertube-video) and one for webtorrent videos (named peertube-playlist).
|
||||
```bash
|
||||
garage bucket create peertube-video
|
||||
garage bucket create peertube-playlist
|
||||
```
|
||||
|
||||
Now we allow our key to read and write on these buckets:
|
||||
|
||||
```
|
||||
garage bucket allow peertube-playlist --read --write --key peertube-key
|
||||
garage bucket allow peertube-video --read --write --key peertube-key
|
||||
```
|
||||
|
||||
Finally, we need to expose these buckets publicly to serve their content to users:
|
||||
|
||||
```bash
|
||||
garage bucket website --allow peertube-playlist
|
||||
garage bucket website --allow peertube-video
|
||||
```
|
||||
|
||||
These buckets are now accessible on the web port (by default 3902) with the following URL: `http://<bucket><root_domain>:<web_port>` where the root domain is defined in your configuration file (by default `.web.garage`). So we have currently the following URLs:
|
||||
* http://peertube-playlist.web.garage:3902
|
||||
* http://peertube-video.web.garage:3902
|
||||
|
||||
Make sure you (will) have a corresponding DNS entry for them.
|
||||
|
||||
### Configure a Reverse Proxy to serve CORS
|
||||
|
||||
Now we will configure a reverse proxy in front of Garage.
|
||||
This is required as we have no other way to serve CORS headers yet.
|
||||
Check the [Configuring a reverse proxy](/cookbook/reverse_proxy.html) section to know how.
|
||||
|
||||
Now make sure that your 2 dns entries are pointing to your reverse proxy.
|
||||
|
||||
### Configure Peertube
|
||||
|
||||
You must edit the file named `config/production.yaml`, we are only modifying the root key named `object_storage`:
|
||||
|
||||
```yaml
|
||||
object_storage:
|
||||
enabled: true
|
||||
|
||||
# Put localhost only if you have a garage instance running on that node
|
||||
endpoint: 'http://localhost:3900' # or "garage.example.com" if you have TLS on port 443
|
||||
|
||||
# This entry has been added by our patch, must be set to true
|
||||
force_path_style: true
|
||||
|
||||
# Garage supports only one region for now, named garage
|
||||
region: 'garage'
|
||||
|
||||
credentials:
|
||||
access_key_id: 'GKxxxx'
|
||||
secret_access_key: 'xxxx'
|
||||
|
||||
max_upload_part: 2GB
|
||||
|
||||
streaming_playlists:
|
||||
bucket_name: 'peertube-playlist'
|
||||
|
||||
# Keep it empty for our example
|
||||
prefix: ''
|
||||
|
||||
# You must fill this field to make Peertube use our reverse proxy/website logic
|
||||
base_url: 'http://peertube-playlist.web.garage' # Example: 'https://mirror.example.com'
|
||||
|
||||
# Same settings but for webtorrent videos
|
||||
videos:
|
||||
bucket_name: 'peertube-video'
|
||||
prefix: ''
|
||||
# You must fill this field to make Peertube use our reverse proxy/website logic
|
||||
base_url: 'http://peertube-video.web.garage'
|
||||
```
|
||||
|
||||
### That's all
|
||||
|
||||
Everything must be configured now, simply restart Peertube and try to upload a video.
|
||||
You must see in your browser console that data are fetched directly from our bucket (through the reverse proxy).
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
*Known bug:* The playback does not start and some 400 Bad Request Errors appear in your browser console and on Garage.
|
||||
If the description of the error contains HTTP Invalid Range: InvalidRange, the error is due to a buggy ffmpeg version.
|
||||
You must avoid the 4.4.0 and use either a newer or older version.
|
||||
|
||||
*Associated issues:* [#137](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/137), [#138](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/138), [#140](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/140). These issues are non blocking.
|
||||
|
||||
*External link:* [Peertube Documentation > Remote Storage](https://docs.joinpeertube.org/admin-remote-storage)
|
||||
|
||||
## Mastodon
|
||||
|
||||
https://docs.joinmastodon.org/admin/config/#cdn
|
||||
|
||||
## Matrix
|
||||
|
||||
Matrix is a chat communication protocol. Its main stable server implementation, [Synapse](https://matrix-org.github.io/synapse/latest/), provides a module to store media on a S3 backend. Additionally, a server independent media store supporting S3 has been developped by the community, it has been made possible thanks to how the matrix API has been designed and will work with implementations like Conduit, Dendrite, etc.
|
||||
|
||||
### synapse-s3-storage-provider (synapse only)
|
||||
|
||||
Supposing you have a working synapse installation, you can add the module with pip:
|
||||
|
||||
```bash
|
||||
pip3 install --user git+https://github.com/matrix-org/synapse-s3-storage-provider.git
|
||||
```
|
||||
|
||||
Now create a bucket and a key for your matrix instance (note your Key ID and Secret Key somewhere, they will be needed later):
|
||||
|
||||
```bash
|
||||
garage key new --name matrix-key
|
||||
garage bucket create matrix
|
||||
garage bucket allow matrix --read --write --key matrix-key
|
||||
```
|
||||
|
||||
Then you must edit your server configuration (eg. `/etc/matrix-synapse/homeserver.yaml`) and add the `media_storage_providers` root key:
|
||||
|
||||
```yaml
|
||||
media_storage_providers:
|
||||
- module: s3_storage_provider.S3StorageProviderBackend
|
||||
store_local: True # do we want to store on S3 media created by our users?
|
||||
store_remote: True # do we want to store on S3 media created
|
||||
# by users of others servers federated to ours?
|
||||
store_synchronous: True # do we want to wait that the file has been written before returning?
|
||||
config:
|
||||
bucket: matrix # the name of our bucket, we chose matrix earlier
|
||||
region_name: garage # only "garage" is supported for the region field
|
||||
endpoint_url: http://localhost:3900 # the path to the S3 endpoint
|
||||
access_key_id: "GKxxx" # your Key ID
|
||||
secret_access_key: "xxxx" # your Secret Key
|
||||
```
|
||||
|
||||
Note that uploaded media will also be stored locally and this behavior can not be deactivated, it is even required for
|
||||
some operations like resizing images.
|
||||
In fact, your local filesysem is considered as a cache but without any automated way to garbage collect it.
|
||||
|
||||
We can build our garbage collector with `s3_media_upload`, a tool provided with the module.
|
||||
If you installed the module with the command provided before, you should be able to bring it in your path:
|
||||
|
||||
```
|
||||
PATH=$HOME/.local/bin/:$PATH
|
||||
command -v s3_media_upload
|
||||
```
|
||||
|
||||
Now we can write a simple script (eg `~/.local/bin/matrix-cache-gc`):
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
## CONFIGURATION ##
|
||||
AWS_ACCESS_KEY_ID=GKxxx
|
||||
AWS_SECRET_ACCESS_KEY=xxxx
|
||||
S3_ENDPOINT=http://localhost:3900
|
||||
S3_BUCKET=matrix
|
||||
MEDIA_STORE=/var/lib/matrix-synapse/media
|
||||
PG_USER=matrix
|
||||
PG_PASS=xxxx
|
||||
PG_DB=synapse
|
||||
PG_HOST=localhost
|
||||
PG_PORT=5432
|
||||
|
||||
## CODE ##
|
||||
PATH=$HOME/.local/bin/:$PATH
|
||||
cat > database.yaml <<EOF
|
||||
user: $PG_USER
|
||||
password: $PG_PASS
|
||||
database: $PG_DB
|
||||
host: $PG_HOST
|
||||
port: $PG_PORT
|
||||
EOF
|
||||
|
||||
s3_media_upload update-db 1d
|
||||
s3_media_upload --no-progress check-deleted $MEDIA_STORE
|
||||
s3_media_upload --no-progress upload $MEDIA_STORE $S3_BUCKET --delete --endpoint-url $S3_ENDPOINT
|
||||
```
|
||||
|
||||
This script will list all the medias that were not accessed in the 24 hours according to your database.
|
||||
It will check if, in this list, the file still exists in the local media store.
|
||||
For files that are still in the cache, it will upload them to S3 if they are not already present (in case of a crash or an initial synchronisation).
|
||||
Finally, the script will delete these files from the cache.
|
||||
|
||||
Make this script executable and check that it works:
|
||||
|
||||
```bash
|
||||
chmod +x $HOME/.local/bin/matrix-cache-gc
|
||||
matrix-cache-gc
|
||||
```
|
||||
|
||||
Add it to your crontab. Open the editor with:
|
||||
|
||||
```bash
|
||||
crontab -e
|
||||
```
|
||||
|
||||
And add a new line. For example, to run it every 10 minutes:
|
||||
|
||||
```cron
|
||||
*/10 * * * * $HOME/.local/bin/matrix-cache-gc
|
||||
```
|
||||
|
||||
*External link:* [Github > matrix-org/synapse-s3-storage-provider](https://github.com/matrix-org/synapse-s3-storage-provider)
|
||||
|
||||
### matrix-media-repo (server independent)
|
||||
|
||||
*External link:* [matrix-media-repo Documentation > S3](https://docs.t2bot.io/matrix-media-repo/configuration/s3-datastore.html)
|
||||
|
||||
## Pixelfed
|
||||
|
||||
https://docs.pixelfed.org/technical-documentation/env.html#filesystem
|
||||
|
||||
## Pleroma
|
||||
|
||||
https://docs-develop.pleroma.social/backend/configuration/cheatsheet/#pleromauploaderss3
|
||||
|
||||
## Lemmy
|
||||
|
||||
via pict-rs
|
||||
https://git.asonix.dog/asonix/pict-rs/commit/f9f4fc63d670f357c93f24147c2ee3e1278e2d97
|
||||
|
||||
## Funkwhale
|
||||
|
||||
https://docs.funkwhale.audio/admin/configuration.html#s3-storage
|
||||
|
||||
## Misskey
|
||||
|
||||
https://github.com/misskey-dev/misskey/commit/9d944243a3a59e8880a360cbfe30fd5a3ec8d52d
|
||||
|
||||
## Prismo
|
||||
|
||||
https://gitlab.com/prismosuite/prismo/-/blob/dev/.env.production.sample#L26-33
|
||||
|
||||
## Owncloud Infinite Scale (ocis)
|
||||
|
||||
## Unsupported
|
||||
|
||||
- Mobilizon: No S3 integration
|
||||
- WriteFreely: No S3 integration
|
||||
- Plume: No S3 integration
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 197 KiB |
@@ -0,0 +1,127 @@
|
||||
# CLI tools
|
||||
|
||||
CLI tools allow you to query the S3 API without too many abstractions.
|
||||
These tools are particularly suitable for debug, backups, website deployments or any scripted task that need to handle data.
|
||||
|
||||
## Minio client (recommended)
|
||||
|
||||
Use the following command to set an "alias", i.e. define a new S3 server to be
|
||||
used by the Minio client:
|
||||
|
||||
```bash
|
||||
mc alias set \
|
||||
garage \
|
||||
<endpoint> \
|
||||
<access key> \
|
||||
<secret key> \
|
||||
--api S3v4
|
||||
```
|
||||
|
||||
Remember that `mc` is sometimes called `mcli` (such as on Arch Linux), to avoid conflicts
|
||||
with Midnight Commander.
|
||||
|
||||
Some commands:
|
||||
|
||||
```bash
|
||||
# list buckets
|
||||
mc ls garage/
|
||||
|
||||
# list objets in a bucket
|
||||
mc ls garage/my_files
|
||||
|
||||
# copy from your filesystem to garage
|
||||
mc cp /proc/cpuinfo garage/my_files/cpuinfo.txt
|
||||
|
||||
# copy from garage to your filesystem
|
||||
mc cp garage/my_files/cpuinfo.txt /tmp/cpuinfo.txt
|
||||
|
||||
# mirror a folder from your filesystem to garage
|
||||
mc mirror --overwrite ./book garage/garagehq.deuxfleurs.fr
|
||||
```
|
||||
|
||||
|
||||
## AWS CLI
|
||||
|
||||
Create a file named `~/.aws/credentials` and put:
|
||||
|
||||
```toml
|
||||
[default]
|
||||
aws_access_key_id=xxxx
|
||||
aws_secret_access_key=xxxx
|
||||
```
|
||||
|
||||
Then a file named `~/.aws/config` and put:
|
||||
|
||||
```toml
|
||||
[default]
|
||||
region=garage
|
||||
```
|
||||
|
||||
Now, supposing Garage is listening on `http://127.0.0.1:3900`, you can list your buckets with:
|
||||
|
||||
```bash
|
||||
aws --endpoint-url http://127.0.0.1:3900 s3 ls
|
||||
```
|
||||
|
||||
Passing the `--endpoint-url` parameter to each command is annoying but AWS developers do not provide a corresponding configuration entry.
|
||||
As a workaround, you can redefine the aws command by editing the file `~/.bashrc`:
|
||||
|
||||
```
|
||||
function aws { command aws --endpoint-url http://127.0.0.1:3900 $@ ; }
|
||||
```
|
||||
|
||||
*Do not forget to run `source ~/.bashrc` or to start a new terminal before running the next commands.*
|
||||
|
||||
Now you can simply run:
|
||||
|
||||
```bash
|
||||
# list buckets
|
||||
aws s3 ls
|
||||
|
||||
# list objects of a bucket
|
||||
aws s3 ls s3://my_files
|
||||
|
||||
# copy from your filesystem to garage
|
||||
aws s3 cp /proc/cpuinfo s3://my_files/cpuinfo.txt
|
||||
|
||||
# copy from garage to your filesystem
|
||||
aws s3 cp s3/my_files/cpuinfo.txt /tmp/cpuinfo.txt
|
||||
```
|
||||
|
||||
## `rclone`
|
||||
|
||||
`rclone` can be configured using the interactive assistant invoked using `rclone configure`.
|
||||
|
||||
You can also configure `rclone` by writing directly its configuration file.
|
||||
Here is a template `rclone.ini` configuration file:
|
||||
|
||||
```ini
|
||||
[garage]
|
||||
type = s3
|
||||
provider = Other
|
||||
env_auth = false
|
||||
access_key_id = <access key>
|
||||
secret_access_key = <secret key>
|
||||
region = <region>
|
||||
endpoint = <endpoint>
|
||||
force_path_style = true
|
||||
acl = private
|
||||
bucket_acl = private
|
||||
```
|
||||
|
||||
## Cyberduck
|
||||
|
||||
TODO
|
||||
|
||||
## `s3cmd`
|
||||
|
||||
Here is a template for the `s3cmd.cfg` file to talk with Garage:
|
||||
|
||||
```ini
|
||||
[default]
|
||||
access_key = <access key>
|
||||
secret_key = <secret key>
|
||||
host_base = <endpoint without http(s)://>
|
||||
host_bucket = <same as host_base>
|
||||
use_https = False | True
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
# Your code (PHP, JS, Go...)
|
||||
@@ -1,4 +1,4 @@
|
||||
# Configuring S3 clients to interact with Garage
|
||||
# Connect it to
|
||||
|
||||
To configure an S3 client to interact with Garage, you will need the following
|
||||
parameters:
|
||||
@@ -28,78 +28,11 @@ provided that you follow the following guidelines:
|
||||
Garage should normally redirect your client to the correct region,
|
||||
but in case your client does not support this you might have to configure it manually.
|
||||
|
||||
We will now provide example configurations for the most common S3 clients.
|
||||
We will now provide example configurations for the most common clients per category:
|
||||
|
||||
## AWS CLI
|
||||
- [Apps](./apps.md)
|
||||
- [Websites](./websites.md)
|
||||
- [Repositories](./repositories.md)
|
||||
- [CLI tools](./cli.md)
|
||||
- [Your code](./code.md)
|
||||
|
||||
Export the following environment variables:
|
||||
|
||||
```bash
|
||||
export AWS_ACCESS_KEY_ID=<access key>
|
||||
export AWS_SECRET_ACCESS_KEY=<secret key>
|
||||
export AWS_DEFAULT_REGION=<region>
|
||||
```
|
||||
|
||||
Now invoke `aws` as follows:
|
||||
|
||||
```bash
|
||||
aws --endpoint-url <endpoint> s3 <command...>
|
||||
```
|
||||
|
||||
For instance: `aws --endpoint-url http://127.0.0.1:3901 s3 ls s3://my-bucket/`.
|
||||
|
||||
## Minio client
|
||||
|
||||
Use the following command to set an "alias", i.e. define a new S3 server to be
|
||||
used by the Minio client:
|
||||
|
||||
```bash
|
||||
mc alias set \
|
||||
garage \
|
||||
<endpoint> \
|
||||
<access key> \
|
||||
<secret key> \
|
||||
--api S3v4
|
||||
```
|
||||
|
||||
Remember that `mc` is sometimes called `mcli` (such as on Arch Linux), to avoid conflicts
|
||||
with the Midnight Commander.
|
||||
|
||||
|
||||
## `rclone`
|
||||
|
||||
`rclone` can be configured using the interactive assistant invoked using `rclone configure`.
|
||||
|
||||
You can also configure `rclone` by writing directly its configuration file.
|
||||
Here is a template `rclone.ini` configuration file:
|
||||
|
||||
```ini
|
||||
[garage]
|
||||
type = s3
|
||||
provider = Other
|
||||
env_auth = false
|
||||
access_key_id = <access key>
|
||||
secret_access_key = <secret key>
|
||||
region = <region>
|
||||
endpoint = <endpoint>
|
||||
force_path_style = true
|
||||
acl = private
|
||||
bucket_acl = private
|
||||
```
|
||||
|
||||
## Cyberduck
|
||||
|
||||
TODO
|
||||
|
||||
## `s3cmd`
|
||||
|
||||
Here is a template for the `s3cmd.cfg` file to talk with Garage:
|
||||
|
||||
```ini
|
||||
[default]
|
||||
access_key = <access key>
|
||||
secret_key = <secret key>
|
||||
host_base = <endpoint without http(s)://>
|
||||
host_bucket = <same as host_base>
|
||||
use_https = False | True
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
# Repositories (Docker, Nix...)
|
||||
@@ -0,0 +1,77 @@
|
||||
# Websites (Hugo, Jekyll, Publii...)
|
||||
|
||||
Garage is also suitable to host static websites.
|
||||
While they can be deployed with traditional CLI tools, some static website generators have integrated options to ease your workflow.
|
||||
|
||||
## Hugo
|
||||
|
||||
Add to your `config.toml` the following section:
|
||||
|
||||
```toml
|
||||
[[deployment.targets]]
|
||||
URL = "s3://<bucket>?endpoint=<endpoint>&disableSSL=<bool>&s3ForcePathStyle=true®ion=garage"
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```toml
|
||||
[[deployment.targets]]
|
||||
URL = "s3://my-blog?endpoint=localhost:9000&disableSSL=true&s3ForcePathStyle=true®ion=garage"
|
||||
```
|
||||
|
||||
Then inform hugo of your credentials:
|
||||
|
||||
```bash
|
||||
export AWS_ACCESS_KEY_ID=GKxxx
|
||||
export AWS_SECRET_ACCESS_KEY=xxx
|
||||
```
|
||||
|
||||
And finally deploy your website:
|
||||
|
||||
```bsh
|
||||
hugo deploy
|
||||
```
|
||||
|
||||
*External links:*
|
||||
- [gocloud.dev > aws > Supported URL parameters](https://pkg.go.dev/gocloud.dev/aws?utm_source=godoc#ConfigFromURLParams)
|
||||
- [Hugo Documentation > hugo deploy](https://gohugo.io/hosting-and-deployment/hugo-deploy/)
|
||||
|
||||
## Publii
|
||||
|
||||
It would require a patch either on Garage or on Publii to make both systems work.
|
||||
|
||||
Currently, the proposed workaround is to deploy your website manually:
|
||||
- On the left menu, click on Server, choose Manual Deployment (the logo looks like a compressed file)
|
||||
- Set your website URL, keep Output type as "Non-compressed catalog"
|
||||
- Click on Save changes
|
||||
- Click on Sync your website (bottom left of the app)
|
||||
- On the new page, click again on Sync your website
|
||||
- Click on Get website files
|
||||
- You need to synchronize the output folder you see in your file explorer, we will use minio client.
|
||||
|
||||
Be sure that you [configured minio client](cli.html#minio-client-recommended).
|
||||
|
||||
Then copy this output folder
|
||||
|
||||
```bash
|
||||
mc mirror --overwrite output garage/my-site
|
||||
```
|
||||
|
||||
## Generic (eg. Jekyll)
|
||||
|
||||
Some tools do not support sending to a S3 backend but output a compiled folder on your system.
|
||||
We can then use any CLI tool to upload this content to our S3 target.
|
||||
|
||||
First, start by [configuring minio client](cli.html#minio-client-recommended).
|
||||
|
||||
Then build your website:
|
||||
|
||||
```bash
|
||||
jekyll build
|
||||
```
|
||||
|
||||
And copy jekyll's output folder on S3:
|
||||
|
||||
```bash
|
||||
mc mirror --overwrite _site garage/my-site
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
# Exposing buckets as websites
|
||||
|
||||
You can expose your bucket as a website with this simple command:
|
||||
|
||||
```bash
|
||||
garage bucket website --allow my-website
|
||||
```
|
||||
|
||||
Now it will be **publicly** exposed on the web endpoint (by default listening on port 3902).
|
||||
|
||||
Our website serving logic is as follow:
|
||||
- Supports only static websites (no support for PHP or other languages)
|
||||
- Does not support directory listing
|
||||
- The index is defined in your `garage.toml`. ([ref](/reference_manual/configuration.html#index))
|
||||
|
||||
Now we need to infer the URL of your website through your bucket name.
|
||||
Let assume:
|
||||
- we set `root_domain = ".web.example.com"` in `garage.toml` ([ref](/reference_manual/configuration.html#root_domain))
|
||||
- our bucket name is `garagehq.deuxfleurs.fr`.
|
||||
|
||||
Our bucket will be served if the Host field matches one of these 2 values (the port is ignored):
|
||||
|
||||
- `garagehq.deuxfleurs.fr.web.example.com`: you can dedicate a subdomain to your users (here `web.example.com`).
|
||||
|
||||
- `garagehq.deuxfleurs.fr`: your users can bring their own domain name, they just need to point them to your Garage cluster.
|
||||
|
||||
You can try this logic locally, without configuring any DNS, thanks to `curl`:
|
||||
|
||||
```bash
|
||||
# prepare your test
|
||||
echo hello world > /tmp/index.html
|
||||
mc cp /tmp/index.html garage/garagehq.deuxfleurs.fr
|
||||
|
||||
curl -H 'Host: garagehq.deuxfleurs.fr' http://localhost:3902
|
||||
# should print "hello world"
|
||||
|
||||
curl -H 'Host: garagehq.deuxfleurs.fr.web.example.com' http://localhost:3902
|
||||
# should also print "hello world"
|
||||
```
|
||||
|
||||
Now that you understand how website logic works on Garage, you can:
|
||||
|
||||
- make the website endpoint listens on port 80 (instead of 3902)
|
||||
- use iptables to redirect the port 80 to the port 3902:
|
||||
`iptables -t nat -A PREROUTING -p tcp -dport 80 -j REDIRECT -to-port 3902`
|
||||
- or configure a [reverse proxy](reverse_proxy.html) in front of Garage to add TLS (HTTPS), CORS support, etc.
|
||||
|
||||
You can also take a look at [Website Integration](/connect/websites.html) to see how you can add Garage to your workflow.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Gateways
|
||||
|
||||
Gateways allow you to expose Garage endpoints (S3 API and websites) without storing data on the node.
|
||||
|
||||
## Benefits
|
||||
|
||||
You can configure Garage as a gateway on all nodes that will consume your S3 API, it will provide you the following benefits:
|
||||
|
||||
- **It removes 1 or 2 network RTT** Instead of (querying your reverse proxy then) querying a random node of the cluster that will forward your request to the nodes effectively storing the data, your local gateway will directly knows which node to query.
|
||||
|
||||
- **It ease server management** Instead of tracking in your reverse proxy and DNS what are the current Garage nodes, your gateway being part of the cluster keeps this information for you. In your software, you will always specify `http://localhost:3900`.
|
||||
|
||||
- **It simplifies security** Instead of having to maintain and renew a TLS certificate, you leverage the Secret Handshake protocol we use for our cluster. The S3 API protocol will be in plain text but limited to your local machine.
|
||||
|
||||
## Limitations
|
||||
|
||||
Currently it will not work with minio client. Follow issue [#64](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/64) for more information.
|
||||
|
||||
## Spawn a Gateway
|
||||
|
||||
The instructions are similar to a regular node, the only option that is different is while configuring the node, you must set the `--gateway` parameter:
|
||||
|
||||
```bash
|
||||
garage node configure --gateway --tag gw1 xxxx
|
||||
```
|
||||
|
||||
Then use `http://localhost:3900` when a S3 endpoint is required:
|
||||
|
||||
```bash
|
||||
aws --endpoint-url http://127.0.0.1:3900 s3 ls
|
||||
```
|
||||
@@ -11,7 +11,7 @@ to get familiar with Garage's command line and usage patterns.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
To run a real-world deployment, make sure you the following conditions are met:
|
||||
To run a real-world deployment, make sure the following conditions are met:
|
||||
|
||||
- You have at least three machines with sufficient storage space available.
|
||||
|
||||
@@ -52,7 +52,6 @@ For example:
|
||||
sudo docker pull lxpz/garage_amd64:v0.4.0
|
||||
```
|
||||
|
||||
|
||||
## Deploying and configuring Garage
|
||||
|
||||
On each machine, we will have a similar setup,
|
||||
@@ -79,9 +78,7 @@ rpc_bind_addr = "[::]:3901"
|
||||
rpc_public_addr = "<this node's public IP>:3901"
|
||||
rpc_secret = "<RPC secret>"
|
||||
|
||||
bootstrap_peers = [
|
||||
# We will fill this in later
|
||||
]
|
||||
bootstrap_peers = []
|
||||
|
||||
[s3_api]
|
||||
s3_region = "garage"
|
||||
@@ -102,33 +99,6 @@ Check the following for your configuration files:
|
||||
- Make sure `rpc_secret` is the same value on all nodes. It should be a 32-bytes hex-encoded secret key.
|
||||
You can generate such a key with `openssl rand -hex 32`.
|
||||
|
||||
You will now have to run `garage node-id` on all nodes to generate node keys.
|
||||
This will print keys as follows:
|
||||
|
||||
```bash
|
||||
Mercury$ garage node-id
|
||||
563e1ac825ee3323aa441e72c26d1030d6d4414aeb3dd25287c531e7fc2bc95d@[fc00:1::1]:3901
|
||||
|
||||
Venus$ garage node-id
|
||||
86f0f26ae4afbd59aaf9cfb059eefac844951efd5b8caeec0d53f4ed6c85f332@[fc00:1::2]:3901
|
||||
|
||||
etc.
|
||||
```
|
||||
|
||||
You can then add these nodes to the `bootstrap_peers` list of at least one of your nodes:
|
||||
|
||||
```toml
|
||||
bootstrap_peers = [
|
||||
"563e1ac825ee3323aa441e72c26d1030d6d4414aeb3dd25287c531e7fc2bc95d@[fc00:1::1]:3901",
|
||||
"86f0f26ae4afbd59aaf9cfb059eefac844951efd5b8caeec0d53f4ed6c85f332@[fc00:1::2]:3901",
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
Check the [configuration file reference documentation](../reference_manual/configuration.md)
|
||||
to learn more about all available configuration options.
|
||||
|
||||
|
||||
## Starting Garage using Docker
|
||||
|
||||
On each machine, you can run the daemon with:
|
||||
@@ -154,7 +124,6 @@ but please check the relase notes before doing so!
|
||||
To upgrade, simply stop and remove this container and
|
||||
start again the command with a new version of Garage.
|
||||
|
||||
|
||||
## Controling the daemon
|
||||
|
||||
The `garage` binary has two purposes:
|
||||
@@ -166,11 +135,51 @@ If your configuration file is at `/etc/garage.toml`, the `garage` binary should
|
||||
|
||||
You can test your `garage` CLI utility by running a simple command such as:
|
||||
|
||||
```
|
||||
```bash
|
||||
garage status
|
||||
```
|
||||
|
||||
You should get something like that as result:
|
||||
At this point, nodes are not yet talking to one another.
|
||||
Your output should therefore look like follows:
|
||||
|
||||
```
|
||||
Mercury$ garage node-id
|
||||
==== HEALTHY NODES ====
|
||||
ID Hostname Address Tag Zone Capacity
|
||||
563e1ac825ee3323… Mercury [fc00:1::1]:3901 NO ROLE ASSIGNED
|
||||
```
|
||||
|
||||
|
||||
## Connecting nodes together
|
||||
|
||||
When your Garage nodes first start, they will generate a local node identifier
|
||||
(based on a public/private key pair).
|
||||
|
||||
To obtain the node identifier of a node, once it is generated,
|
||||
run `garage node-id`.
|
||||
This will print keys as follows:
|
||||
|
||||
```bash
|
||||
Mercury$ garage node-id
|
||||
563e1ac825ee3323aa441e72c26d1030d6d4414aeb3dd25287c531e7fc2bc95d@[fc00:1::1]:3901
|
||||
|
||||
Venus$ garage node-id
|
||||
86f0f26ae4afbd59aaf9cfb059eefac844951efd5b8caeec0d53f4ed6c85f332@[fc00:1::2]:3901
|
||||
|
||||
etc.
|
||||
```
|
||||
|
||||
You can then instruct nodes to connect to one another as follows:
|
||||
|
||||
```bash
|
||||
# Instruct Venus to connect to Mercury (this will establish communication both ways)
|
||||
Venus$ garage node connect 563e1ac825ee3323aa441e72c26d1030d6d4414aeb3dd25287c531e7fc2bc95d@[fc00:1::1]:3901
|
||||
```
|
||||
|
||||
You don't nead to instruct all node to connect to all other nodes:
|
||||
nodes will discover one another transitively.
|
||||
|
||||
Now if your run `garage status` on any node, you should have an output that looks as follows:
|
||||
|
||||
```
|
||||
==== HEALTHY NODES ====
|
||||
@@ -181,13 +190,13 @@ ID Hostname Address Tag Zone Capa
|
||||
212f7572f0c89da9… Mars [fc00:F::1]:3901 NO ROLE ASSIGNED
|
||||
```
|
||||
|
||||
|
||||
## Configuring a cluster
|
||||
## Giving roles to nodes
|
||||
|
||||
We will now inform Garage of the disk space available on each node of the cluster
|
||||
as well as the zone (e.g. datacenter) in which each machine is located.
|
||||
|
||||
For our example, we will suppose we have the following infrastructure (Capacity, Identifier and Datacenter are specific values to Garage described in the following):
|
||||
For our example, we will suppose we have the following infrastructure
|
||||
(Capacity, Identifier and Zone are specific values to Garage described in the following):
|
||||
|
||||
| Location | Name | Disk Space | `Capacity` | `Identifier` | `Zone` |
|
||||
|----------|---------|------------|------------|--------------|--------------|
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
# Configuring a reverse proxy
|
||||
|
||||
The main reason to add a reverse proxy in front of Garage is to provide TLS to your users.
|
||||
|
||||
In production you will likely need your certificates signed by a certificate authority.
|
||||
The most automated way is to use a provider supporting the [ACME protocol](https://datatracker.ietf.org/doc/html/rfc8555)
|
||||
such as [Let's Encrypt](https://letsencrypt.org/), [ZeroSSL](https://zerossl.com/) or [Buypass Go SSL](https://www.buypass.com/ssl/products/acme).
|
||||
|
||||
If you are only testing Garage, you can generate a self-signed certificate to follow the documentation:
|
||||
|
||||
```bash
|
||||
openssl req \
|
||||
-new \
|
||||
-x509 \
|
||||
-keyout /tmp/garage.key \
|
||||
-out /tmp/garage.crt \
|
||||
-nodes \
|
||||
-subj "/C=XX/ST=XX/L=XX/O=XX/OU=XX/CN=localhost/emailAddress=X@X.XX" \
|
||||
-addext "subjectAltName = DNS:localhost, IP:127.0.0.1"
|
||||
|
||||
cat /tmp/garage.key /tmp/garage.crt > /tmp/garage.pem
|
||||
```
|
||||
|
||||
Be careful as you will need to allow self signed certificates in your client.
|
||||
For example, with minio, you must add the `--insecure` flag.
|
||||
An example:
|
||||
|
||||
```bash
|
||||
mc ls --insecure garage/
|
||||
```
|
||||
|
||||
## socat (only for testing purposes)
|
||||
|
||||
If you want to test Garage with a TLS frontend, socat can do it for you in a single command:
|
||||
|
||||
```bash
|
||||
socat \
|
||||
"openssl-listen:443,\
|
||||
reuseaddr,\
|
||||
fork,\
|
||||
verify=0,\
|
||||
cert=/tmp/garage.pem" \
|
||||
tcp4-connect:localhost:3900
|
||||
```
|
||||
|
||||
## Nginx
|
||||
|
||||
Nginx is a well-known reverse proxy suitable for production.
|
||||
We do the configuration in 3 steps: first we define the upstream blocks ("the backends")
|
||||
then we define the server blocks ("the frontends") for the S3 endpoint and finally for the web endpoint.
|
||||
|
||||
The following configuration blocks can be all put in the same `/etc/nginx/sites-available/garage.conf`.
|
||||
To make your configuration active, run `ln -s /etc/nginx/sites-available/garage.conf /etc/nginx/sites-enabled/`.
|
||||
If you directly put the instructions in the root `nginx.conf`, keep in mind that these configurations must be enclosed inside a `http { }` block.
|
||||
|
||||
And do not forget to reload nginx with `systemctl reload nginx` or `nginx -s reload`.
|
||||
|
||||
### Defining backends
|
||||
|
||||
First, we need to tell to nginx how to access our Garage cluster.
|
||||
Because we have multiple nodes, we want to leverage all of them by spreading the load.
|
||||
|
||||
In nginx, we can do that with the upstream directive.
|
||||
Because we have 2 endpoints: one for the S3 API and one to serve websites,
|
||||
we create 2 backends named respectively `s3_backend` and `web_backend`.
|
||||
|
||||
A documented example for the `s3_backend` assuming you chose port 3900:
|
||||
|
||||
```nginx
|
||||
upstream s3_backend {
|
||||
# if you have a garage instance locally
|
||||
server 127.0.0.1:3900;
|
||||
# you can also put your other instances
|
||||
server 192.168.1.3:3900;
|
||||
# domain names also work
|
||||
server garage1.example.com:3900;
|
||||
# you can assign weights if you have some servers
|
||||
# that are more powerful than others
|
||||
server garage2.example.com:3900 weight=2;
|
||||
}
|
||||
```
|
||||
|
||||
A similar example for the `web_backend` assuming you chose port 3902:
|
||||
|
||||
```nginx
|
||||
upstream web_backend {
|
||||
server 127.0.0.1:3902;
|
||||
server 192.168.1.3:3902;
|
||||
server garage1.example.com:3902;
|
||||
server garage2.example.com:3902 weight=2;
|
||||
}
|
||||
```
|
||||
|
||||
### Exposing the S3 API
|
||||
|
||||
The configuration section for the S3 API is simple as we only support path-access style yet.
|
||||
We simply configure the TLS parameters and forward all the requests to the backend:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen [::]:443 http2 ssl;
|
||||
ssl_certificate /tmp/garage.crt;
|
||||
ssl_certificate_key /tmp/garage.key;
|
||||
|
||||
# should be the endpoint you want
|
||||
# aws uses s3.amazonaws.com for example
|
||||
server_name garage.example.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://s3_backend;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Exposing the web endpoint
|
||||
|
||||
The web endpoint is a bit more complicated to configure as it listens on many different `Host` fields.
|
||||
To better understand the logic involved, you can refer to the [Exposing buckets as websites](/cookbook/exposing_websites.html) section.
|
||||
Also, for some applications, you may need to serve CORS headers: Garage can not serve them directly but we show how we can use nginx to serve them.
|
||||
You can use the following example as your starting point:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen [::]:443 http2 ssl;
|
||||
ssl_certificate /tmp/garage.crt;
|
||||
ssl_certificate_key /tmp/garage.key;
|
||||
|
||||
# We list all the Hosts fields that can access our buckets
|
||||
server_name *.web.garage
|
||||
example.com
|
||||
my-site.tld
|
||||
;
|
||||
|
||||
location / {
|
||||
# Add these headers only if you want to allow CORS requests
|
||||
# For production use, more specific rules would be better for your security
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header Access-Control-Max-Age 3600;
|
||||
add_header Access-Control-Expose-Headers Content-Length;
|
||||
add_header Access-Control-Allow-Headers Range;
|
||||
|
||||
# We do not forward OPTIONS requests to Garage
|
||||
# as it does not support them but they are needed for CORS.
|
||||
if ($request_method = OPTIONS) {
|
||||
return 200;
|
||||
}
|
||||
|
||||
proxy_pass http://web_backend;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Apache httpd
|
||||
|
||||
@TODO
|
||||
|
||||
## Traefik
|
||||
|
||||
@TODO
|
||||
@@ -1,9 +1,14 @@
|
||||
# Starting Garage with systemd instead of Docker
|
||||
# Starting Garage with systemd
|
||||
|
||||
We make some assumptions for this systemd deployment.
|
||||
|
||||
- Your garage binary is located at `/usr/local/bin/garage`.
|
||||
|
||||
- Your configuration file is located at `/etc/garage.toml`.
|
||||
|
||||
- Your `garage.toml` must be set with `metadata_dir=/var/lib/garage/meta` and `data_dir=/var/lib/garage/data`. This is mandatory to use `systemd` hardening feature [Dynamic User](https://0pointer.net/blog/dynamic-users-with-systemd.html). Note that in your host filesystem, Garage data will be held in `/var/lib/private/garage`.
|
||||
|
||||
NOTE: This guide is incomplete. Typicall you would also want to create a separate
|
||||
Unix user to run Garage.
|
||||
|
||||
Make sure you have the Garage binary installed on your system (see [quick start](../quick_start/index.md)), e.g. at `/usr/local/bin/garage`.
|
||||
|
||||
Create a file named `/etc/systemd/system/garage.service`:
|
||||
|
||||
@@ -15,12 +20,18 @@ Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Environment='RUST_LOG=garage=info' 'RUST_BACKTRACE=1'
|
||||
ExecStart=/usr/local/bin/garage server -c /etc/garage/garage.toml
|
||||
ExecStart=/usr/local/bin/garage server
|
||||
StateDirectory=garage
|
||||
DynamicUser=true
|
||||
ProtectHome=true
|
||||
NoNewPrivileges=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
*A note on hardening: garage will be run as a non privileged user, its user id is dynamically allocated by systemd. It cannot access (read or write) home folders (/home, /root and /run/user), the rest of the filesystem can only be read but not written, only the path seen as /var/lib/garage is writable as seen by the service (mapped to /var/lib/private/garage on your host). Additionnaly, the process can not gain new privileges over time.*
|
||||
|
||||
To start the service then automatically enable it at boot:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
# Design draft
|
||||
|
||||
**WARNING: this documentation is a design draft which was written before Garage's actual implementation.
|
||||
The general principle are similar, but details have not been updated.**
|
||||
|
||||
|
||||
#### Modules
|
||||
|
||||
- `membership/`: configuration, membership management (gossip of node's presence and status), ring generation --> what about Serf (used by Consul/Nomad) : https://www.serf.io/? Seems a huge library with many features so maybe overkill/hard to integrate
|
||||
- `metadata/`: metadata management
|
||||
- `blocks/`: block management, writing, GC and rebalancing
|
||||
- `internal/`: server to server communication (HTTP server and client that reuses connections, TLS if we want, etc)
|
||||
- `api/`: S3 API
|
||||
- `web/`: web management interface
|
||||
|
||||
#### Metadata tables
|
||||
|
||||
**Objects:**
|
||||
|
||||
- *Hash key:* Bucket name (string)
|
||||
- *Sort key:* Object key (string)
|
||||
- *Sort key:* Version timestamp (int)
|
||||
- *Sort key:* Version UUID (string)
|
||||
- Complete: bool
|
||||
- Inline: bool, true for objects < threshold (say 1024)
|
||||
- Object size (int)
|
||||
- Mime type (string)
|
||||
- Data for inlined objects (blob)
|
||||
- Hash of first block otherwise (string)
|
||||
|
||||
*Having only a hash key on the bucket name will lead to storing all file entries of this table for a specific bucket on a single node. At the same time, it is the only way I see to rapidly being able to list all bucket entries...*
|
||||
|
||||
**Blocks:**
|
||||
|
||||
- *Hash key:* Version UUID (string)
|
||||
- *Sort key:* Offset of block in total file (int)
|
||||
- Hash of data block (string)
|
||||
|
||||
A version is defined by the existence of at least one entry in the blocks table for a certain version UUID.
|
||||
We must keep the following invariant: if a version exists in the blocks table, it has to be referenced in the objects table.
|
||||
We explicitly manage concurrent versions of an object: the version timestamp and version UUID columns are index columns, thus we may have several concurrent versions of an object.
|
||||
Important: before deleting an older version from the objects table, we must make sure that we did a successfull delete of the blocks of that version from the blocks table.
|
||||
|
||||
Thus, the workflow for reading an object is as follows:
|
||||
|
||||
1. Check permissions (LDAP)
|
||||
2. Read entry in object table. If data is inline, we have its data, stop here.
|
||||
-> if several versions, take newest one and launch deletion of old ones in background
|
||||
3. Read first block from cluster. If size <= 1 block, stop here.
|
||||
4. Simultaneously with previous step, if size > 1 block: query the Blocks table for the IDs of the next blocks
|
||||
5. Read subsequent blocks from cluster
|
||||
|
||||
Workflow for PUT:
|
||||
|
||||
1. Check write permission (LDAP)
|
||||
2. Select a new version UUID
|
||||
3. Write a preliminary entry for the new version in the objects table with complete = false
|
||||
4. Send blocks to cluster and write entries in the blocks table
|
||||
5. Update the version with complete = true and all of the accurate information (size, etc)
|
||||
6. Return success to the user
|
||||
7. Launch a background job to check and delete older versions
|
||||
|
||||
Workflow for DELETE:
|
||||
|
||||
1. Check write permission (LDAP)
|
||||
2. Get current version (or versions) in object table
|
||||
3. Do the deletion of those versions NOT IN A BACKGROUND JOB THIS TIME
|
||||
4. Return succes to the user if we were able to delete blocks from the blocks table and entries from the object table
|
||||
|
||||
To delete a version:
|
||||
|
||||
1. List the blocks from Cassandra
|
||||
2. For each block, delete it from cluster. Don't care if some deletions fail, we can do GC.
|
||||
3. Delete all of the blocks from the blocks table
|
||||
4. Finally, delete the version from the objects table
|
||||
|
||||
Known issue: if someone is reading from a version that we want to delete and the object is big, the read might be interrupted. I think it is ok to leave it like this, we just cut the connection if data disappears during a read.
|
||||
|
||||
("Soit P un problème, on s'en fout est une solution à ce problème")
|
||||
|
||||
#### Block storage on disk
|
||||
|
||||
**Blocks themselves:**
|
||||
|
||||
- file path = /blobs/(first 3 hex digits of hash)/(rest of hash)
|
||||
|
||||
**Reverse index for GC & other block-level metadata:**
|
||||
|
||||
- file path = /meta/(first 3 hex digits of hash)/(rest of hash)
|
||||
- map block hash -> set of version UUIDs where it is referenced
|
||||
|
||||
Usefull metadata:
|
||||
|
||||
- list of versions that reference this block in the Casandra table, so that we can do GC by checking in Cassandra that the lines still exist
|
||||
- list of other nodes that we know have acknowledged a write of this block, usefull in the rebalancing algorithm
|
||||
|
||||
Write strategy: have a single thread that does all write IO so that it is serialized (or have several threads that manage independent parts of the hash space). When writing a blob, write it to a temporary file, close, then rename so that a concurrent read gets a consistent result (either not found or found with whole content).
|
||||
|
||||
Read strategy: the only read operation is get(hash) that returns either the data or not found (can do a corruption check as well and return corrupted state if it is the case). Can be done concurrently with writes.
|
||||
|
||||
**Internal API:**
|
||||
|
||||
- get(block hash) -> ok+data/not found/corrupted
|
||||
- put(block hash & data, version uuid + offset) -> ok/error
|
||||
- put with no data(block hash, version uuid + offset) -> ok/not found plz send data/error
|
||||
- delete(block hash, version uuid + offset) -> ok/error
|
||||
|
||||
GC: when last ref is deleted, delete block.
|
||||
Long GC procedure: check in Cassandra that version UUIDs still exist and references this block.
|
||||
|
||||
Rebalancing: takes as argument the list of newly added nodes.
|
||||
|
||||
- List all blocks that we have. For each block:
|
||||
- If it hits a newly introduced node, send it to them.
|
||||
Use put with no data first to check if it has to be sent to them already or not.
|
||||
Use a random listing order to avoid race conditions (they do no harm but we might have two nodes sending the same thing at the same time thus wasting time).
|
||||
- If it doesn't hit us anymore, delete it and its reference list.
|
||||
|
||||
Only one balancing can be running at a same time. It can be restarted at the beginning with new parameters.
|
||||
|
||||
#### Membership management
|
||||
|
||||
Two sets of nodes:
|
||||
|
||||
- set of nodes from which a ping was recently received, with status: number of stored blocks, request counters, error counters, GC%, rebalancing%
|
||||
(eviction from this set after say 30 seconds without ping)
|
||||
- set of nodes that are part of the system, explicitly modified by the operator using the web UI (persisted to disk),
|
||||
is a CRDT using a version number for the value of the whole set
|
||||
|
||||
Thus, three states for nodes:
|
||||
|
||||
- healthy: in both sets
|
||||
- missing: not pingable but part of desired cluster
|
||||
- unused/draining: currently present but not part of the desired cluster, empty = if contains nothing, draining = if still contains some blocks
|
||||
|
||||
Membership messages between nodes:
|
||||
|
||||
- ping with current state + hash of current membership info -> reply with same info
|
||||
- send&get back membership info (the ids of nodes that are in the two sets): used when no local membership change in a long time and membership info hash discrepancy detected with first message (passive membership fixing with full CRDT gossip)
|
||||
- inform of newly pingable node(s) -> no result, when receive new info repeat to all (reliable broadcast)
|
||||
- inform of operator membership change -> no result, when receive new info repeat to all (reliable broadcast)
|
||||
|
||||
Ring: generated from the desired set of nodes, however when doing read/writes on the ring, skip nodes that are known to be not pingable.
|
||||
The tokens are generated in a deterministic fashion from node IDs (hash of node id + token number from 1 to K).
|
||||
Number K of tokens per node: decided by the operator & stored in the operator's list of nodes CRDT. Default value proposal: with node status information also broadcast disk total size and free space, and propose a default number of tokens equal to 80%Free space / 10Gb. (this is all user interface)
|
||||
|
||||
|
||||
#### Constants
|
||||
|
||||
- Block size: around 1MB ? --> Exoscale use 16MB chunks
|
||||
- Number of tokens in the hash ring: one every 10Gb of allocated storage
|
||||
- Threshold for storing data directly in Cassandra objects table: 1kb bytes (maybe up to 4kb?)
|
||||
- Ping timeout (time after which a node is registered as unresponsive/missing): 30 seconds
|
||||
- Ping interval: 10 seconds
|
||||
- ??
|
||||
|
||||
#### Links
|
||||
|
||||
- CDC: <https://www.usenix.org/system/files/conference/atc16/atc16-paper-xia.pdf>
|
||||
- Erasure coding: <http://web.eecs.utk.edu/~jplank/plank/papers/CS-08-627.html>
|
||||
- [Openstack Storage Concepts](https://docs.openstack.org/arch-design/design-storage/design-storage-concepts.html)
|
||||
- [RADOS](https://ceph.com/wp-content/uploads/2016/08/weil-rados-pdsw07.pdf)
|
||||
@@ -1,158 +1,95 @@
|
||||
**WARNING: this documentation is more a "design draft", which was written before Garage's actual implementation. The general principle is similar but details have not yet been updated.**
|
||||
# Internals
|
||||
|
||||
#### Modules
|
||||
## Overview
|
||||
|
||||
- `membership/`: configuration, membership management (gossip of node's presence and status), ring generation --> what about Serf (used by Consul/Nomad) : https://www.serf.io/? Seems a huge library with many features so maybe overkill/hard to integrate
|
||||
- `metadata/`: metadata management
|
||||
- `blocks/`: block management, writing, GC and rebalancing
|
||||
- `internal/`: server to server communication (HTTP server and client that reuses connections, TLS if we want, etc)
|
||||
- `api/`: S3 API
|
||||
- `web/`: web management interface
|
||||
TODO: write this section
|
||||
|
||||
#### Metadata tables
|
||||
- The Dynamo ring
|
||||
|
||||
**Objects:**
|
||||
- CRDTs
|
||||
|
||||
- *Hash key:* Bucket name (string)
|
||||
- *Sort key:* Object key (string)
|
||||
- *Sort key:* Version timestamp (int)
|
||||
- *Sort key:* Version UUID (string)
|
||||
- Complete: bool
|
||||
- Inline: bool, true for objects < threshold (say 1024)
|
||||
- Object size (int)
|
||||
- Mime type (string)
|
||||
- Data for inlined objects (blob)
|
||||
- Hash of first block otherwise (string)
|
||||
- Consistency model of Garage tables
|
||||
|
||||
*Having only a hash key on the bucket name will lead to storing all file entries of this table for a specific bucket on a single node. At the same time, it is the only way I see to rapidly being able to list all bucket entries...*
|
||||
|
||||
**Blocks:**
|
||||
|
||||
- *Hash key:* Version UUID (string)
|
||||
- *Sort key:* Offset of block in total file (int)
|
||||
- Hash of data block (string)
|
||||
|
||||
A version is defined by the existence of at least one entry in the blocks table for a certain version UUID.
|
||||
We must keep the following invariant: if a version exists in the blocks table, it has to be referenced in the objects table.
|
||||
We explicitly manage concurrent versions of an object: the version timestamp and version UUID columns are index columns, thus we may have several concurrent versions of an object.
|
||||
Important: before deleting an older version from the objects table, we must make sure that we did a successfull delete of the blocks of that version from the blocks table.
|
||||
|
||||
Thus, the workflow for reading an object is as follows:
|
||||
|
||||
1. Check permissions (LDAP)
|
||||
2. Read entry in object table. If data is inline, we have its data, stop here.
|
||||
-> if several versions, take newest one and launch deletion of old ones in background
|
||||
3. Read first block from cluster. If size <= 1 block, stop here.
|
||||
4. Simultaneously with previous step, if size > 1 block: query the Blocks table for the IDs of the next blocks
|
||||
5. Read subsequent blocks from cluster
|
||||
|
||||
Workflow for PUT:
|
||||
|
||||
1. Check write permission (LDAP)
|
||||
2. Select a new version UUID
|
||||
3. Write a preliminary entry for the new version in the objects table with complete = false
|
||||
4. Send blocks to cluster and write entries in the blocks table
|
||||
5. Update the version with complete = true and all of the accurate information (size, etc)
|
||||
6. Return success to the user
|
||||
7. Launch a background job to check and delete older versions
|
||||
|
||||
Workflow for DELETE:
|
||||
|
||||
1. Check write permission (LDAP)
|
||||
2. Get current version (or versions) in object table
|
||||
3. Do the deletion of those versions NOT IN A BACKGROUND JOB THIS TIME
|
||||
4. Return succes to the user if we were able to delete blocks from the blocks table and entries from the object table
|
||||
|
||||
To delete a version:
|
||||
|
||||
1. List the blocks from Cassandra
|
||||
2. For each block, delete it from cluster. Don't care if some deletions fail, we can do GC.
|
||||
3. Delete all of the blocks from the blocks table
|
||||
4. Finally, delete the version from the objects table
|
||||
|
||||
Known issue: if someone is reading from a version that we want to delete and the object is big, the read might be interrupted. I think it is ok to leave it like this, we just cut the connection if data disappears during a read.
|
||||
|
||||
("Soit P un problème, on s'en fout est une solution à ce problème")
|
||||
|
||||
#### Block storage on disk
|
||||
|
||||
**Blocks themselves:**
|
||||
|
||||
- file path = /blobs/(first 3 hex digits of hash)/(rest of hash)
|
||||
|
||||
**Reverse index for GC & other block-level metadata:**
|
||||
|
||||
- file path = /meta/(first 3 hex digits of hash)/(rest of hash)
|
||||
- map block hash -> set of version UUIDs where it is referenced
|
||||
|
||||
Usefull metadata:
|
||||
|
||||
- list of versions that reference this block in the Casandra table, so that we can do GC by checking in Cassandra that the lines still exist
|
||||
- list of other nodes that we know have acknowledged a write of this block, usefull in the rebalancing algorithm
|
||||
|
||||
Write strategy: have a single thread that does all write IO so that it is serialized (or have several threads that manage independent parts of the hash space). When writing a blob, write it to a temporary file, close, then rename so that a concurrent read gets a consistent result (either not found or found with whole content).
|
||||
|
||||
Read strategy: the only read operation is get(hash) that returns either the data or not found (can do a corruption check as well and return corrupted state if it is the case). Can be done concurrently with writes.
|
||||
|
||||
**Internal API:**
|
||||
|
||||
- get(block hash) -> ok+data/not found/corrupted
|
||||
- put(block hash & data, version uuid + offset) -> ok/error
|
||||
- put with no data(block hash, version uuid + offset) -> ok/not found plz send data/error
|
||||
- delete(block hash, version uuid + offset) -> ok/error
|
||||
|
||||
GC: when last ref is deleted, delete block.
|
||||
Long GC procedure: check in Cassandra that version UUIDs still exist and references this block.
|
||||
|
||||
Rebalancing: takes as argument the list of newly added nodes.
|
||||
|
||||
- List all blocks that we have. For each block:
|
||||
- If it hits a newly introduced node, send it to them.
|
||||
Use put with no data first to check if it has to be sent to them already or not.
|
||||
Use a random listing order to avoid race conditions (they do no harm but we might have two nodes sending the same thing at the same time thus wasting time).
|
||||
- If it doesn't hit us anymore, delete it and its reference list.
|
||||
|
||||
Only one balancing can be running at a same time. It can be restarted at the beginning with new parameters.
|
||||
|
||||
#### Membership management
|
||||
|
||||
Two sets of nodes:
|
||||
|
||||
- set of nodes from which a ping was recently received, with status: number of stored blocks, request counters, error counters, GC%, rebalancing%
|
||||
(eviction from this set after say 30 seconds without ping)
|
||||
- set of nodes that are part of the system, explicitly modified by the operator using the web UI (persisted to disk),
|
||||
is a CRDT using a version number for the value of the whole set
|
||||
|
||||
Thus, three states for nodes:
|
||||
|
||||
- healthy: in both sets
|
||||
- missing: not pingable but part of desired cluster
|
||||
- unused/draining: currently present but not part of the desired cluster, empty = if contains nothing, draining = if still contains some blocks
|
||||
|
||||
Membership messages between nodes:
|
||||
|
||||
- ping with current state + hash of current membership info -> reply with same info
|
||||
- send&get back membership info (the ids of nodes that are in the two sets): used when no local membership change in a long time and membership info hash discrepancy detected with first message (passive membership fixing with full CRDT gossip)
|
||||
- inform of newly pingable node(s) -> no result, when receive new info repeat to all (reliable broadcast)
|
||||
- inform of operator membership change -> no result, when receive new info repeat to all (reliable broadcast)
|
||||
|
||||
Ring: generated from the desired set of nodes, however when doing read/writes on the ring, skip nodes that are known to be not pingable.
|
||||
The tokens are generated in a deterministic fashion from node IDs (hash of node id + token number from 1 to K).
|
||||
Number K of tokens per node: decided by the operator & stored in the operator's list of nodes CRDT. Default value proposal: with node status information also broadcast disk total size and free space, and propose a default number of tokens equal to 80%Free space / 10Gb. (this is all user interface)
|
||||
See this presentation (in French) for some first information:
|
||||
<https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/main/doc/talks/2020-12-02_wide-team/talk.pdf>
|
||||
|
||||
|
||||
#### Constants
|
||||
## Garbage collection
|
||||
|
||||
- Block size: around 1MB ? --> Exoscale use 16MB chunks
|
||||
- Number of tokens in the hash ring: one every 10Gb of allocated storage
|
||||
- Threshold for storing data directly in Cassandra objects table: 1kb bytes (maybe up to 4kb?)
|
||||
- Ping timeout (time after which a node is registered as unresponsive/missing): 30 seconds
|
||||
- Ping interval: 10 seconds
|
||||
- ??
|
||||
A faulty garbage collection procedure has been the cause of
|
||||
[critical bug #39](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/39).
|
||||
This precise bug was fixed in the code, however there are potentially more
|
||||
general issues with the garbage collector being too eager and deleting things
|
||||
too early. This has been the subject of
|
||||
[PR #135](https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/135).
|
||||
This section summarizes the discussions on this topic.
|
||||
|
||||
#### Links
|
||||
Rationale: we want to ensure Garage's safety by making sure things don't get
|
||||
deleted from disk if they are still needed. Two aspects are involved in this.
|
||||
|
||||
### 1. Garbage collection of table entries (in `meta/` directory)
|
||||
|
||||
The `Entry` trait used for table entries (defined in `tables/schema.rs`)
|
||||
defines a function `is_tombstone()` that returns `true` if that entry
|
||||
represents an entry that is deleted in the table. CRDT semantics by default
|
||||
keep all tombstones, because they are necessary for reconciliation: if node A
|
||||
has a tombstone that supersedes a value `x`, and node B has value `x`, A has to
|
||||
keep the tombstone in memory so that the value `x` can be properly deleted at
|
||||
node `B`. Otherwise, due to the CRDT reconciliation rule, the value `x` from B
|
||||
would flow back to A and a deleted item would reappear in the system.
|
||||
|
||||
Here, we have some control on the nodes involved in storing Garage data.
|
||||
Therefore we have a garbage collector that is able to delete tombstones UNDER
|
||||
CERTAIN CONDITIONS. This garbage collector is implemented in `table/gc.rs`. To
|
||||
delete a tombstone, the following condition has to be met:
|
||||
|
||||
- All nodes responsible for storing this entry are aware of the existence of
|
||||
the tombstone, i.e. they cannot hold another version of the entry that is
|
||||
superseeded by the tombstone. This ensures that deleting the tombstone is
|
||||
safe and that no deleted value will come back in the system.
|
||||
|
||||
Garage makes use of Sled's atomic operations (such as compare-and-swap and
|
||||
transactions) to ensure that only tombstones that have been correctly
|
||||
propagated to other nodes are ever deleted from the local entry tree.
|
||||
|
||||
This GC is safe in the following sense: no non-tombstone data is ever deleted
|
||||
from Garage tables.
|
||||
|
||||
**However**, there is an issue with the way this interacts with data
|
||||
rebalancing in the case when a partition is moving between nodes. If a node has
|
||||
some data of a partition for which it is not responsible, it has to offload it.
|
||||
However that offload process takes some time. In that interval, the GC does not
|
||||
check with that node if it has the tombstone before deleting the tombstone, so
|
||||
perhaps it doesn't have it and when the offload finally happens, old data comes
|
||||
back in the system.
|
||||
|
||||
**PR 135 mostly fixes this** by implementing a 24-hour delay before anything is
|
||||
garbage collected in a table. This works under the assumption that rebalances
|
||||
that follow data shuffling terminate in less than 24 hours.
|
||||
|
||||
**However**, in distributed systems, it is generally considered a bad practice
|
||||
to make assumptions that information propagates in a certain time interval:
|
||||
this consists in making a synchrony assumption, meaning that we are basically
|
||||
assuming a computing model that has much stronger properties than otherwise. To
|
||||
maximize the applicability of Garage, we would like to remove this assumption,
|
||||
and implement a system where time does not play a role. To do this, we would
|
||||
need to find a way to safely disable the GC when data is being shuffled around,
|
||||
and safely detect that the shuffling has terminated and thus the GC can be
|
||||
resumed. This introduces some complexity to the protocol and hasn't been
|
||||
tackled yet.
|
||||
|
||||
### 2. Garbage collection of data blocks (in `data/` directory)
|
||||
|
||||
Blocks in the data directory are reference-counted. In Garage versions before
|
||||
PR #135, blocks could get deleted from local disk as soon as their reference
|
||||
counter reached zero. We had a mechanism to not trigger this immediately at the
|
||||
rc-reaches-zero event, but the cleanup could be triggered by other means (for
|
||||
example by a block repair operation...). PR #135 added a safety measure so that
|
||||
blocks never get deleted in a 10 minute interval following the time when the RC
|
||||
reaches zero. This is a measure to make impossible race conditions such as #39.
|
||||
We would have liked to use a larger delay (e.g. 24 hours), but in the case of a
|
||||
rebalance of data, this would have led to the disk utilization to explode
|
||||
during the rebalancing, only to shrink again after 24 hours. The 10-minute
|
||||
delay is a compromise that gives good security while not having this problem of
|
||||
disk space explosion on rebalance.
|
||||
|
||||
- CDC: <https://www.usenix.org/system/files/conference/atc16/atc16-paper-xia.pdf>
|
||||
- Erasure coding: <http://web.eecs.utk.edu/~jplank/plank/papers/CS-08-627.html>
|
||||
- [Openstack Storage Concepts](https://docs.openstack.org/arch-design/design-storage/design-storage-concepts.html)
|
||||
- [RADOS](https://ceph.com/wp-content/uploads/2016/08/weil-rados-pdsw07.pdf)
|
||||
|
||||
@@ -92,10 +92,21 @@ caching our development dependencies.
|
||||
*Currently there is no automatic garbage collection of the cache: we should monitor its growth.
|
||||
Hopefully, we can erase it totally without breaking any build, the next build will only be slower.*
|
||||
|
||||
In practise, we concluded that we do not want to cache all the compilation dependencies.
|
||||
Instead, we want to cache the toolchain we use to build Garage each time we change it.
|
||||
So we removed from Drone any automatic update of the cache and instead handle them manually with:
|
||||
|
||||
```
|
||||
source ~/.awsrc
|
||||
nix-shell --run 'refresh_toolchain'
|
||||
```
|
||||
|
||||
Internally, it will run `nix-build` on `nix/toolchain.nix` and send the output plus its depedencies to the cache.
|
||||
|
||||
To erase the cache:
|
||||
|
||||
```
|
||||
mc rm --recursive --force 'garage/nix/*'
|
||||
mc rm --recursive --force 'garage/nix/'
|
||||
```
|
||||
|
||||
### Publishing Garage
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
</p>
|
||||
|
||||
<p align="center" style="text-align:center;">
|
||||
[ <a href="https://git.deuxfleurs.fr/Deuxfleurs/garage">Git repository</a>
|
||||
[ <a href="https://garagehq.deuxfleurs.fr/_releases.html">Download</a>
|
||||
| <a href="https://git.deuxfleurs.fr/Deuxfleurs/garage">Git repository</a>
|
||||
| <a href="https://matrix.to/#/%23garage:deuxfleurs.fr">Matrix channel</a>
|
||||
| <a href="https://drone.deuxfleurs.fr/Deuxfleurs/garage">Drone CI</a>
|
||||
]
|
||||
@@ -114,7 +115,7 @@ Please note that if you patch Garage and then use it to provide any service over
|
||||
|
||||
# Funding
|
||||
|
||||
The Deuxfleurs association has received a grant promise to fund 3 people working on Garage for a year, from October 2021 to September 2022.
|
||||
The Deuxfleurs association has received a grant from [NGI POINTER](https://pointer.ngi.eu/), to fund 3 people working on Garage full-time for a year: from October 2021 to September 2022.
|
||||
|
||||
<div style="display: flex; justify-content: space-around">
|
||||
<a href="https://pointer.ngi.eu/">
|
||||
@@ -125,4 +126,4 @@ The Deuxfleurs association has received a grant promise to fund 3 people working
|
||||
</a>
|
||||
</div>
|
||||
|
||||
_This project has received funding from the European Union’s Horizon 2020 research and innovation programme within the framework of the NGI-POINTER Project funded under grant agreement No 871528._
|
||||
_This project has received funding from the European Union’s Horizon 2020 research and innovation programme within the framework of the NGI-POINTER Project funded under grant agreement N° 871528._
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# Migrating from 0.3 to 0.4
|
||||
|
||||
**Migrating from 0.3 to 0.4 is unsupported. This document is only intended to
|
||||
document the process internally for the Deuxfleurs cluster where we have to do
|
||||
it. Do not try it yourself, you will lose your data and we will not help you.**
|
||||
|
||||
**Migrating from 0.2 to 0.4 will break everything for sure. Never try it.**
|
||||
|
||||
The internal data format of Garage hasn't changed much between 0.3 and 0.4.
|
||||
The Sled database is still the same, and the data directory as well.
|
||||
|
||||
The following has changed, all in the meta directory:
|
||||
|
||||
- `node_id` in 0.3 contains the identifier of the current node. In 0.4, this
|
||||
file does nothing and should be deleted. It is replaced by `node_key` (the
|
||||
secret key) and `node_key.pub` (the associated public key). A node's
|
||||
identifier on the ring is its public key.
|
||||
|
||||
- `peer_info` in 0.3 contains the list of peers saved automatically by Garage.
|
||||
The format has changed and it is now stored in `peer_list` (`peer_info`
|
||||
should be deleted).
|
||||
|
||||
When migrating, all node identifiers will change. This also means that the
|
||||
affectation of data partitions on the ring will change, and lots of data will
|
||||
have to be rebalanced.
|
||||
|
||||
- If your cluster has only 3 nodes, all nodes store everything, therefore nothing has to be rebalanced.
|
||||
|
||||
- If your cluster has only 4 nodes, for any partition there will always be at
|
||||
least 2 nodes that stored data before that still store it after. Therefore
|
||||
the migration should in theory be transparent and Garage should continue to
|
||||
work during the rebalance.
|
||||
|
||||
- If your cluster has 5 or more nodes, data will disappear during the
|
||||
migration. Do not migrate (fortunately we don't have this scenario at
|
||||
Deuxfleurs), or if you do, make Garage unavailable until things stabilize
|
||||
(disable web and api access).
|
||||
|
||||
|
||||
The migration steps are as follows:
|
||||
|
||||
1. Prepare a new configuration file for 0.4. For each node, point to the same
|
||||
meta and data directories as Garage 0.3. Basically, the things that change
|
||||
are the following:
|
||||
|
||||
- No more `rpc_tls` section
|
||||
- You have to generate a shared `rpc_secret` and put it in all config files
|
||||
- `bootstrap_peers` has a different syntax as it has to contain node keys.
|
||||
Leave it empty and use `garage node-id` and `garage node connect` instead (new features of 0.4)
|
||||
- put the publicly accessible RPC address of your node in `rpc_public_addr` if possible (its optional but recommended)
|
||||
- If you are using Consul, change the `consul_service_name` to NOT be the name advertised by Nomad.
|
||||
Now Garage is responsible for advertising its own service itself.
|
||||
|
||||
2. Disable api and web access for some time (Garage does not support disabling
|
||||
these endpoints but you can change the port number or stop your reverse
|
||||
proxy for instance).
|
||||
|
||||
3. Do `garage repair -a --yes tables` and `garage repair -a --yes blocks`,
|
||||
check the logs and check that all data seems to be synced correctly between
|
||||
nodes.
|
||||
|
||||
4. Save somewhere the output of `garage status`. We will need this to remember
|
||||
how to reconfigure nodes in 0.4.
|
||||
|
||||
5. Turn off Garage 0.3
|
||||
|
||||
6. Backup metadata folders if you can (i.e. if you have space to do it
|
||||
somewhere). Backuping data folders could also be usefull but that's much
|
||||
harder to do. If your filesystem supports snapshots, this could be a good
|
||||
time to use them.
|
||||
|
||||
7. Turn on Garage 0.4
|
||||
|
||||
8. At this point, running `garage status` should indicate that all nodes of the
|
||||
previous cluster are "unavailable". The nodes have new identifiers that
|
||||
should appear in healthy nodes once they can talk to one another (use
|
||||
`garage node connect` if necessary`). They should have NO ROLE ASSIGNED at
|
||||
the moment.
|
||||
|
||||
9. Prepare a script with several `garage node configure` commands that replace
|
||||
each of the v0.3 node ID with the corresponding v0.4 node ID, with the same
|
||||
zone/tag/capacity. For example if your node `drosera` had identifier `c24e`
|
||||
before and now has identifier `789a`, and it was configured with capacity
|
||||
`2` in zone `dc1`, put the following command in your script:
|
||||
|
||||
```bash
|
||||
garage node configure 789a -z dc1 -c 2 -t drosera --replace c24e
|
||||
```
|
||||
|
||||
10. Run your reconfiguration script. Check that the new output of `garage
|
||||
status` contains the correct node IDs with the correct values for capacity
|
||||
and zone. Old nodes should no longer be mentioned.
|
||||
|
||||
11. If your cluster has 4 nodes or less, and you are feeling adventurous, you
|
||||
can reenable Web and API access now. Things will probably work.
|
||||
|
||||
12. Garage might already be resyncing stuff. Issue a `garage repair -a --yes
|
||||
tables` and `garage repair -a --yes blocks` to force it to do so.
|
||||
|
||||
13. Wait for resyncing activity to stop in the logs. Do steps 12 and 13 two or
|
||||
three times, until you see that when you issue the repair commands, nothing
|
||||
gets resynced any longer.
|
||||
|
||||
14. Your upgraded cluster should be in a working state. Re-enable API and Web
|
||||
access and check that everything went well.
|
||||
@@ -12,16 +12,10 @@ rec {
|
||||
url = "https://github.com/superboum/cargo2nix";
|
||||
rev = "1364752cd784764db2ef5b1e1248727cebfae2ce";
|
||||
};
|
||||
rustOverlaySrc = fetchGit {
|
||||
# As of 2021-10-05
|
||||
url = "https://github.com/oxalica/rust-overlay";
|
||||
rev = "9c2fc6a62ccbc6f420d71ecac6bf0b84dbbee64f";
|
||||
};
|
||||
|
||||
/*
|
||||
* Shared objects
|
||||
*/
|
||||
rustOverlay = import rustOverlaySrc;
|
||||
cargo2nix = import cargo2nixSrc;
|
||||
cargo2nixOverlay = import "${cargo2nixSrc}/overlay";
|
||||
}
|
||||
|
||||
@@ -2,6 +2,3 @@ substituters = https://cache.nixos.org https://nix.web.deuxfleurs.fr
|
||||
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= nix.web.deuxfleurs.fr:eTGL6kvaQn6cDR/F9lDYUIP9nCVR/kkshYfLDJf1yKs=
|
||||
max-jobs = auto
|
||||
cores = 4
|
||||
|
||||
# required for containers
|
||||
sandbox = false
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
system ? builtins.currentSystem,
|
||||
}:
|
||||
|
||||
with import ./common.nix;
|
||||
|
||||
let
|
||||
platforms = [
|
||||
"x86_64-unknown-linux-musl"
|
||||
"i686-unknown-linux-musl"
|
||||
"aarch64-unknown-linux-musl"
|
||||
"armv6l-unknown-linux-musleabihf"
|
||||
];
|
||||
pkgsList = builtins.map (target: import pkgsSrc {
|
||||
inherit system;
|
||||
crossSystem = { config = target; };
|
||||
}) platforms;
|
||||
pkgsHost = import pkgsSrc {};
|
||||
lib = pkgsHost.lib;
|
||||
kaniko = (import ./kaniko.nix) pkgsHost;
|
||||
in
|
||||
lib.flatten (builtins.map (pkgs: [
|
||||
pkgs.rustPlatform.rust.rustc
|
||||
pkgs.rustPlatform.rust.cargo
|
||||
pkgs.buildPackages.stdenv.cc
|
||||
]) pkgsList) ++ [
|
||||
kaniko
|
||||
]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
system ? builtins.currentSystem,
|
||||
crossSystem ? null,
|
||||
rust ? true,
|
||||
integration ? true,
|
||||
release ? true,
|
||||
@@ -10,10 +9,9 @@ with import ./nix/common.nix;
|
||||
|
||||
let
|
||||
pkgs = import pkgsSrc {
|
||||
inherit system crossSystem;
|
||||
overlays = [ cargo2nixOverlay rustOverlay ];
|
||||
inherit system;
|
||||
overlays = [ cargo2nixOverlay ];
|
||||
};
|
||||
rustDist = pkgs.buildPackages.rust-bin.stable.latest.default;
|
||||
kaniko = (import ./nix/kaniko.nix) pkgs;
|
||||
|
||||
in
|
||||
@@ -57,13 +55,39 @@ function refresh_index {
|
||||
result \
|
||||
s3://garagehq.deuxfleurs.fr/_releases.html
|
||||
}
|
||||
|
||||
function refresh_toolchain {
|
||||
nix copy \
|
||||
--to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/etc/nix/signing-key.sec' \
|
||||
$(nix-store -qR \
|
||||
$(nix-build --quiet --no-build-output --no-out-link nix/toolchain.nix))
|
||||
}
|
||||
'';
|
||||
|
||||
nativeBuildInputs =
|
||||
(if rust then [ rustDist (pkgs.callPackage cargo2nix {}).package ] else [])
|
||||
(if rust then [
|
||||
pkgs.rustPlatform.rust.rustc
|
||||
pkgs.rustPlatform.rust.cargo
|
||||
pkgs.clippy
|
||||
pkgs.rustfmt
|
||||
/*(pkgs.callPackage cargo2nix {}).package*/
|
||||
] else [])
|
||||
++
|
||||
(if integration then [ pkgs.s3cmd pkgs.awscli2 pkgs.minio-client pkgs.rclone pkgs.socat pkgs.psmisc pkgs.which ] else [])
|
||||
(if integration then [
|
||||
pkgs.s3cmd
|
||||
pkgs.awscli2
|
||||
pkgs.minio-client
|
||||
pkgs.rclone
|
||||
pkgs.socat
|
||||
pkgs.psmisc
|
||||
pkgs.which
|
||||
pkgs.openssl
|
||||
pkgs.curl
|
||||
] else [])
|
||||
++
|
||||
(if release then [ pkgs.awscli2 kaniko ] else [])
|
||||
(if release then [
|
||||
pkgs.awscli2
|
||||
kaniko
|
||||
] else [])
|
||||
;
|
||||
}
|
||||
|
||||
+16
-19
@@ -39,7 +39,7 @@ pub async fn run_api_server(
|
||||
}
|
||||
});
|
||||
|
||||
let server = Server::bind(&addr).serve(service);
|
||||
let server = Server::bind(addr).serve(service);
|
||||
|
||||
let graceful = server.with_graceful_shutdown(shutdown_signal);
|
||||
info!("API server listening on http://{}", addr);
|
||||
@@ -88,8 +88,8 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
||||
|
||||
let (bucket, key) = parse_bucket_key(&path)?;
|
||||
let allowed = match req.method() {
|
||||
&Method::HEAD | &Method::GET => api_key.allow_read(&bucket),
|
||||
_ => api_key.allow_write(&bucket),
|
||||
&Method::HEAD | &Method::GET => api_key.allow_read(bucket),
|
||||
_ => api_key.allow_write(bucket),
|
||||
};
|
||||
if !allowed {
|
||||
return Err(Error::Forbidden(
|
||||
@@ -109,11 +109,11 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
||||
match *req.method() {
|
||||
Method::HEAD => {
|
||||
// HeadObject query
|
||||
Ok(handle_head(garage, &req, &bucket, &key).await?)
|
||||
Ok(handle_head(garage, &req, bucket, key).await?)
|
||||
}
|
||||
Method::GET => {
|
||||
// GetObject query
|
||||
Ok(handle_get(garage, &req, &bucket, &key).await?)
|
||||
Ok(handle_get(garage, &req, bucket, key).await?)
|
||||
}
|
||||
Method::PUT => {
|
||||
if params.contains_key(&"partnumber".to_string())
|
||||
@@ -125,8 +125,8 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
||||
Ok(handle_put_part(
|
||||
garage,
|
||||
req,
|
||||
&bucket,
|
||||
&key,
|
||||
bucket,
|
||||
key,
|
||||
part_number,
|
||||
upload_id,
|
||||
content_sha256,
|
||||
@@ -136,46 +136,43 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
||||
// CopyObject query
|
||||
let copy_source = req.headers().get("x-amz-copy-source").unwrap().to_str()?;
|
||||
let copy_source =
|
||||
percent_encoding::percent_decode_str(©_source).decode_utf8()?;
|
||||
percent_encoding::percent_decode_str(copy_source).decode_utf8()?;
|
||||
let (source_bucket, source_key) = parse_bucket_key(©_source)?;
|
||||
if !api_key.allow_read(&source_bucket) {
|
||||
if !api_key.allow_read(source_bucket) {
|
||||
return Err(Error::Forbidden(format!(
|
||||
"Reading from bucket {} not allowed for this key",
|
||||
source_bucket
|
||||
)));
|
||||
}
|
||||
let source_key = source_key.ok_or_bad_request("No source key specified")?;
|
||||
Ok(
|
||||
handle_copy(garage, &req, &bucket, &key, &source_bucket, &source_key)
|
||||
.await?,
|
||||
)
|
||||
Ok(handle_copy(garage, &req, bucket, key, source_bucket, source_key).await?)
|
||||
} else {
|
||||
// PutObject query
|
||||
Ok(handle_put(garage, req, &bucket, &key, content_sha256).await?)
|
||||
Ok(handle_put(garage, req, bucket, key, content_sha256).await?)
|
||||
}
|
||||
}
|
||||
Method::DELETE => {
|
||||
if params.contains_key(&"uploadid".to_string()) {
|
||||
// AbortMultipartUpload query
|
||||
let upload_id = params.get("uploadid").unwrap();
|
||||
Ok(handle_abort_multipart_upload(garage, &bucket, &key, upload_id).await?)
|
||||
Ok(handle_abort_multipart_upload(garage, bucket, key, upload_id).await?)
|
||||
} else {
|
||||
// DeleteObject query
|
||||
Ok(handle_delete(garage, &bucket, &key).await?)
|
||||
Ok(handle_delete(garage, bucket, key).await?)
|
||||
}
|
||||
}
|
||||
Method::POST => {
|
||||
if params.contains_key(&"uploads".to_string()) {
|
||||
// CreateMultipartUpload call
|
||||
Ok(handle_create_multipart_upload(garage, &req, &bucket, &key).await?)
|
||||
Ok(handle_create_multipart_upload(garage, &req, bucket, key).await?)
|
||||
} else if params.contains_key(&"uploadid".to_string()) {
|
||||
// CompleteMultipartUpload call
|
||||
let upload_id = params.get("uploadid").unwrap();
|
||||
Ok(handle_complete_multipart_upload(
|
||||
garage,
|
||||
req,
|
||||
&bucket,
|
||||
&key,
|
||||
bucket,
|
||||
key,
|
||||
upload_id,
|
||||
content_sha256,
|
||||
)
|
||||
|
||||
@@ -55,7 +55,8 @@ async fn handle_delete_internal(
|
||||
);
|
||||
|
||||
garage.object_table.insert(&object).await?;
|
||||
return Ok((deleted_version, version_uuid));
|
||||
|
||||
Ok((deleted_version, version_uuid))
|
||||
}
|
||||
|
||||
pub async fn handle_delete(
|
||||
@@ -82,7 +83,7 @@ pub async fn handle_delete_objects(
|
||||
let body = hyper::body::to_bytes(req.into_body()).await?;
|
||||
verify_signed_content(content_sha256, &body[..])?;
|
||||
|
||||
let cmd_xml = roxmltree::Document::parse(&std::str::from_utf8(&body)?)?;
|
||||
let cmd_xml = roxmltree::Document::parse(std::str::from_utf8(&body)?)?;
|
||||
let cmd = parse_delete_objects_xml(&cmd_xml).ok_or_bad_request("Invalid delete XML query")?;
|
||||
|
||||
let mut ret_deleted = Vec::new();
|
||||
|
||||
+5
-5
@@ -106,12 +106,12 @@ pub async fn handle_head(
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if let Some(cached) = try_answer_cached(&version, version_meta, req) {
|
||||
if let Some(cached) = try_answer_cached(version, version_meta, req) {
|
||||
return Ok(cached);
|
||||
}
|
||||
|
||||
let body: Body = Body::empty();
|
||||
let response = object_headers(&version, version_meta)
|
||||
let response = object_headers(version, version_meta)
|
||||
.header("Content-Length", format!("{}", version_meta.size))
|
||||
.status(StatusCode::OK)
|
||||
.body(body)
|
||||
@@ -149,7 +149,7 @@ pub async fn handle_get(
|
||||
ObjectVersionData::FirstBlock(meta, _) => meta,
|
||||
};
|
||||
|
||||
if let Some(cached) = try_answer_cached(&last_v, last_v_meta, req) {
|
||||
if let Some(cached) = try_answer_cached(last_v, last_v_meta, req) {
|
||||
return Ok(cached);
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ pub async fn handle_get(
|
||||
.await;
|
||||
}
|
||||
|
||||
let resp_builder = object_headers(&last_v, last_v_meta)
|
||||
let resp_builder = object_headers(last_v, last_v_meta)
|
||||
.header("Content-Length", format!("{}", last_v_meta.size))
|
||||
.status(StatusCode::OK);
|
||||
|
||||
@@ -190,7 +190,7 @@ pub async fn handle_get(
|
||||
Ok(resp_builder.body(body)?)
|
||||
}
|
||||
ObjectVersionData::FirstBlock(_, first_block_hash) => {
|
||||
let read_first_block = garage.block_manager.rpc_get_block(&first_block_hash);
|
||||
let read_first_block = garage.block_manager.rpc_get_block(first_block_hash);
|
||||
let get_next_blocks = garage.version_table.get(&last_v.uuid, &EmptyKey);
|
||||
|
||||
let (first_block, version) = futures::try_join!(read_first_block, get_next_blocks)?;
|
||||
|
||||
+5
-5
@@ -193,8 +193,8 @@ async fn read_and_put_blocks(
|
||||
|
||||
let mut next_offset = first_block.len();
|
||||
let mut put_curr_version_block = put_block_meta(
|
||||
&garage,
|
||||
&version,
|
||||
garage,
|
||||
version,
|
||||
part_number,
|
||||
0,
|
||||
first_block_hash,
|
||||
@@ -213,8 +213,8 @@ async fn read_and_put_blocks(
|
||||
let block_hash = blake2sum(&block[..]);
|
||||
let block_len = block.len();
|
||||
put_curr_version_block = put_block_meta(
|
||||
&garage,
|
||||
&version,
|
||||
garage,
|
||||
version,
|
||||
part_number,
|
||||
next_offset as u64,
|
||||
block_hash,
|
||||
@@ -437,7 +437,7 @@ pub async fn handle_complete_multipart_upload(
|
||||
let body = hyper::body::to_bytes(req.into_body()).await?;
|
||||
verify_signed_content(content_sha256, &body[..])?;
|
||||
|
||||
let body_xml = roxmltree::Document::parse(&std::str::from_utf8(&body)?)?;
|
||||
let body_xml = roxmltree::Document::parse(std::str::from_utf8(&body)?)?;
|
||||
let body_list_of_parts = parse_complete_multpart_upload_body(&body_xml)
|
||||
.ok_or_bad_request("Invalid CompleteMultipartUpload XML")?;
|
||||
debug!(
|
||||
|
||||
@@ -70,7 +70,7 @@ pub async fn check_signature(
|
||||
let canonical_request = canonical_request(
|
||||
request.method(),
|
||||
&request.uri().path().to_string(),
|
||||
&canonical_query_string(&request.uri()),
|
||||
&canonical_query_string(request.uri()),
|
||||
&headers,
|
||||
&authorization.signed_headers,
|
||||
&authorization.content_sha256,
|
||||
@@ -252,7 +252,7 @@ fn canonical_request(
|
||||
method.as_str(),
|
||||
url_path,
|
||||
canonical_query_string,
|
||||
&canonical_header_string(&headers, signed_headers),
|
||||
&canonical_header_string(headers, signed_headers),
|
||||
"",
|
||||
signed_headers,
|
||||
content_sha256,
|
||||
|
||||
@@ -41,4 +41,5 @@ futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
||||
|
||||
netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
|
||||
#netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
|
||||
netapp = "0.3.0"
|
||||
|
||||
+1
-1
@@ -446,7 +446,7 @@ impl AdminRpcHandler {
|
||||
if opt.detailed {
|
||||
writeln!(
|
||||
&mut ret,
|
||||
" number of blocks: {}",
|
||||
" number of RC entries (~= number of blocks): {}",
|
||||
self.garage.block_manager.rc_len()
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -93,14 +93,17 @@ pub async fn cmd_status(rpc_cli: &Endpoint<SystemRpc, ()>, rpc_host: NodeID) ->
|
||||
for adv in status.iter().filter(|adv| !adv.is_up) {
|
||||
if let Some(cfg) = config.members.get(&adv.id) {
|
||||
failed_nodes.push(format!(
|
||||
"{id:?}\t{host}\t{addr}\t[{tag}]\t{zone}\t{capacity}\t{last_seen}s ago",
|
||||
"{id:?}\t{host}\t{addr}\t[{tag}]\t{zone}\t{capacity}\t{last_seen}",
|
||||
id = adv.id,
|
||||
host = adv.status.hostname,
|
||||
addr = adv.addr,
|
||||
tag = cfg.tag,
|
||||
zone = cfg.zone,
|
||||
capacity = cfg.capacity_string(),
|
||||
last_seen = "FIXME", // FIXME was (now_msec() - adv.last_seen) / 1000,
|
||||
last_seen = adv
|
||||
.last_seen_secs_ago
|
||||
.map(|s| format!("{}s ago", s))
|
||||
.unwrap_or_else(|| "never seen".into()),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use log::warn;
|
||||
|
||||
use garage_util::error::*;
|
||||
|
||||
pub const READ_KEY_ERROR: &str = "Unable to read node key. It will be generated by your garage node the first time is it launched. Ensure that your garage node is currently running. (The node key is supposed to be stored in your metadata directory.)";
|
||||
@@ -22,11 +24,11 @@ pub fn node_id_command(config_file: PathBuf, quiet: bool) -> Result<(), Error> {
|
||||
println!("{}", idstr);
|
||||
|
||||
if !quiet {
|
||||
eprintln!("WARNING: I don't know the public address to reach this node.");
|
||||
eprintln!("In all of the instructions below, replace 127.0.0.1:3901 by the appropriate address and port.");
|
||||
warn!("WARNING: I don't know the public address to reach this node.");
|
||||
warn!("In all of the instructions below, replace 127.0.0.1:{} by the appropriate address and port.", config.rpc_bind_addr.port());
|
||||
}
|
||||
|
||||
format!("{}@127.0.0.1:3901", idstr)
|
||||
format!("{}@127.0.0.1:{}", idstr, config.rpc_bind_addr.port())
|
||||
};
|
||||
|
||||
if !quiet {
|
||||
|
||||
@@ -8,8 +8,7 @@ pub enum Command {
|
||||
#[structopt(name = "server")]
|
||||
Server,
|
||||
|
||||
/// Print identifier (public key) of this garage node.
|
||||
/// Generates a new keypair if necessary.
|
||||
/// Print identifier (public key) of this Garage node
|
||||
#[structopt(name = "node-id")]
|
||||
NodeId(NodeIdOpt),
|
||||
|
||||
@@ -265,7 +264,7 @@ pub struct RepairOpt {
|
||||
pub yes: bool,
|
||||
|
||||
#[structopt(subcommand)]
|
||||
pub what: Option<RepairWhat>,
|
||||
pub what: RepairWhat,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
|
||||
@@ -282,6 +281,13 @@ pub enum RepairWhat {
|
||||
/// Only redo the propagation of version deletions to the block ref table (extremely slow)
|
||||
#[structopt(name = "block_refs")]
|
||||
BlockRefs,
|
||||
/// Verify integrity of all blocks on disc (extremely slow, i/o intensive)
|
||||
#[structopt(name = "scrub")]
|
||||
Scrub {
|
||||
/// Tranquility factor (see tranquilizer documentation)
|
||||
#[structopt(name = "tranquility", default_value = "2")]
|
||||
tranquility: u32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, StructOpt, Debug, Clone)]
|
||||
|
||||
@@ -54,7 +54,7 @@ pub fn format_table(data: Vec<String>) {
|
||||
out.push_str(col);
|
||||
(0..col_len - col.chars().count() + 2).for_each(|_| out.push(' '));
|
||||
}
|
||||
out.push_str(&row[row.len() - 1]);
|
||||
out.push_str(row[row.len() - 1]);
|
||||
out.push('\n');
|
||||
}
|
||||
|
||||
|
||||
+28
-8
@@ -9,6 +9,7 @@ mod cli;
|
||||
mod repair;
|
||||
mod server;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use structopt::StructOpt;
|
||||
@@ -29,15 +30,20 @@ use cli::*;
|
||||
struct Opt {
|
||||
/// Host to connect to for admin operations, in the format:
|
||||
/// <public-key>@<ip>:<port>
|
||||
#[structopt(short = "h", long = "rpc-host")]
|
||||
#[structopt(short = "h", long = "rpc-host", env = "GARAGE_RPC_HOST")]
|
||||
pub rpc_host: Option<String>,
|
||||
|
||||
/// RPC secret network key for admin operations
|
||||
#[structopt(short = "s", long = "rpc-secret")]
|
||||
#[structopt(short = "s", long = "rpc-secret", env = "GARAGE_RPC_SECRET")]
|
||||
pub rpc_secret: Option<String>,
|
||||
|
||||
/// Configuration file (garage.toml)
|
||||
#[structopt(short = "c", long = "config", default_value = "/etc/garage.toml")]
|
||||
#[structopt(
|
||||
short = "c",
|
||||
long = "config",
|
||||
env = "GARAGE_CONFIG_FILE",
|
||||
default_value = "/etc/garage.toml"
|
||||
)]
|
||||
pub config_file: PathBuf,
|
||||
|
||||
#[structopt(subcommand)]
|
||||
@@ -46,6 +52,9 @@ struct Opt {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "garage=info")
|
||||
}
|
||||
pretty_env_logger::init();
|
||||
sodiumoxide::init().expect("Unable to init sodiumoxide");
|
||||
|
||||
@@ -99,12 +108,23 @@ async fn cli_command(opt: Opt) -> Result<(), Error> {
|
||||
let (id, addr) = if let Some(h) = opt.rpc_host {
|
||||
let (id, addrs) = parse_and_resolve_peer_addr(&h).ok_or_else(|| format!("Invalid RPC remote node identifier: {}. Expected format is <pubkey>@<IP or hostname>:<port>.", h))?;
|
||||
(id, addrs[0])
|
||||
} else if let Some(a) = config.as_ref().map(|c| c.rpc_public_addr).flatten() {
|
||||
let node_id = garage_rpc::system::read_node_id(&config.unwrap().metadata_dir)
|
||||
.err_context(READ_KEY_ERROR)?;
|
||||
(node_id, a)
|
||||
} else {
|
||||
return Err(Error::Message("No RPC host provided".into()));
|
||||
let node_id = garage_rpc::system::read_node_id(&config.as_ref().unwrap().metadata_dir)
|
||||
.err_context(READ_KEY_ERROR)?;
|
||||
if let Some(a) = config.as_ref().map(|c| c.rpc_public_addr).flatten() {
|
||||
(node_id, a)
|
||||
} else {
|
||||
let default_addr = SocketAddr::new(
|
||||
"127.0.0.1".parse().unwrap(),
|
||||
config.as_ref().unwrap().rpc_bind_addr.port(),
|
||||
);
|
||||
warn!(
|
||||
"Trying to contact Garage node at default address {}",
|
||||
default_addr
|
||||
);
|
||||
warn!("If this doesn't work, consider adding rpc_public_addr in your config file or specifying the -h command line parameter.");
|
||||
(node_id, default_addr)
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to target host
|
||||
|
||||
+31
-41
@@ -27,42 +27,38 @@ impl Repair {
|
||||
opt: RepairOpt,
|
||||
must_exit: watch::Receiver<bool>,
|
||||
) -> Result<(), Error> {
|
||||
let todo = |x| opt.what.as_ref().map(|y| *y == x).unwrap_or(true);
|
||||
|
||||
if todo(RepairWhat::Tables) {
|
||||
info!("Launching a full sync of tables");
|
||||
self.garage.bucket_table.syncer.add_full_sync();
|
||||
self.garage.object_table.syncer.add_full_sync();
|
||||
self.garage.version_table.syncer.add_full_sync();
|
||||
self.garage.block_ref_table.syncer.add_full_sync();
|
||||
self.garage.key_table.syncer.add_full_sync();
|
||||
match opt.what {
|
||||
RepairWhat::Tables => {
|
||||
info!("Launching a full sync of tables");
|
||||
self.garage.bucket_table.syncer.add_full_sync();
|
||||
self.garage.object_table.syncer.add_full_sync();
|
||||
self.garage.version_table.syncer.add_full_sync();
|
||||
self.garage.block_ref_table.syncer.add_full_sync();
|
||||
self.garage.key_table.syncer.add_full_sync();
|
||||
}
|
||||
RepairWhat::Versions => {
|
||||
info!("Repairing the versions table");
|
||||
self.repair_versions(&must_exit).await?;
|
||||
}
|
||||
RepairWhat::BlockRefs => {
|
||||
info!("Repairing the block refs table");
|
||||
self.repair_block_ref(&must_exit).await?;
|
||||
}
|
||||
RepairWhat::Blocks => {
|
||||
info!("Repairing the stored blocks");
|
||||
self.garage
|
||||
.block_manager
|
||||
.repair_data_store(&must_exit)
|
||||
.await?;
|
||||
}
|
||||
RepairWhat::Scrub { tranquility } => {
|
||||
info!("Verifying integrity of stored blocks");
|
||||
self.garage
|
||||
.block_manager
|
||||
.scrub_data_store(&must_exit, tranquility)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: wait for full sync to finish before proceeding to the rest?
|
||||
|
||||
if todo(RepairWhat::Versions) {
|
||||
info!("Repairing the versions table");
|
||||
self.repair_versions(&must_exit).await?;
|
||||
}
|
||||
|
||||
if todo(RepairWhat::BlockRefs) {
|
||||
info!("Repairing the block refs table");
|
||||
self.repair_block_ref(&must_exit).await?;
|
||||
}
|
||||
|
||||
if opt.what.is_none() {
|
||||
info!("Repairing the RC");
|
||||
self.repair_rc(&must_exit).await?;
|
||||
}
|
||||
|
||||
if todo(RepairWhat::Blocks) {
|
||||
info!("Repairing the stored blocks");
|
||||
self.garage
|
||||
.block_manager
|
||||
.repair_data_store(&must_exit)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -150,10 +146,4 @@ impl Repair {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn repair_rc(&self, _must_exit: &watch::Receiver<bool>) -> Result<(), Error> {
|
||||
// TODO
|
||||
warn!("repair_rc: not implemented");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,4 +33,5 @@ futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
||||
|
||||
netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
|
||||
#netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
|
||||
netapp = "0.3.0"
|
||||
|
||||
+599
-337
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -33,6 +33,7 @@ futures-util = "0.3"
|
||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
||||
tokio-stream = { version = "0.1", features = ["net"] }
|
||||
|
||||
netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
|
||||
#netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
|
||||
netapp = "0.3.0"
|
||||
hyper = { version = "0.14", features = ["client", "http1", "runtime", "tcp"] }
|
||||
|
||||
|
||||
+179
-45
@@ -7,6 +7,7 @@ use futures::stream::futures_unordered::FuturesUnordered;
|
||||
use futures::stream::StreamExt;
|
||||
use futures_util::future::FutureExt;
|
||||
use tokio::select;
|
||||
use tokio::sync::{watch, Semaphore};
|
||||
|
||||
pub use netapp::endpoint::{Endpoint, EndpointHandler, Message as Rpc};
|
||||
use netapp::peering::fullmesh::FullMeshPeeringStrategy;
|
||||
@@ -14,11 +15,18 @@ pub use netapp::proto::*;
|
||||
pub use netapp::{NetApp, NodeID};
|
||||
|
||||
use garage_util::background::BackgroundRunner;
|
||||
use garage_util::data::Uuid;
|
||||
use garage_util::data::*;
|
||||
use garage_util::error::Error;
|
||||
|
||||
use crate::ring::Ring;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
// Try to never have more than 200MB of outgoing requests
|
||||
// buffered at the same time. Other requests are queued until
|
||||
// space is freed.
|
||||
const REQUEST_BUFFER_SIZE: usize = 200 * 1024 * 1024;
|
||||
|
||||
/// Strategy to apply when making RPC
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct RequestStrategy {
|
||||
@@ -61,12 +69,32 @@ impl RequestStrategy {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RpcHelper {
|
||||
pub(crate) fullmesh: Arc<FullMeshPeeringStrategy>,
|
||||
pub(crate) background: Arc<BackgroundRunner>,
|
||||
pub struct RpcHelper(Arc<RpcHelperInner>);
|
||||
|
||||
struct RpcHelperInner {
|
||||
our_node_id: Uuid,
|
||||
fullmesh: Arc<FullMeshPeeringStrategy>,
|
||||
background: Arc<BackgroundRunner>,
|
||||
ring: watch::Receiver<Arc<Ring>>,
|
||||
request_buffer_semaphore: Semaphore,
|
||||
}
|
||||
|
||||
impl RpcHelper {
|
||||
pub(crate) fn new(
|
||||
our_node_id: Uuid,
|
||||
fullmesh: Arc<FullMeshPeeringStrategy>,
|
||||
background: Arc<BackgroundRunner>,
|
||||
ring: watch::Receiver<Arc<Ring>>,
|
||||
) -> Self {
|
||||
Self(Arc::new(RpcHelperInner {
|
||||
our_node_id,
|
||||
fullmesh,
|
||||
background,
|
||||
ring,
|
||||
request_buffer_semaphore: Semaphore::new(REQUEST_BUFFER_SIZE),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn call<M, H, S>(
|
||||
&self,
|
||||
endpoint: &Endpoint<M, H>,
|
||||
@@ -92,10 +120,23 @@ impl RpcHelper {
|
||||
M: Rpc<Response = Result<S, Error>>,
|
||||
H: EndpointHandler<M>,
|
||||
{
|
||||
let msg_size = rmp_to_vec_all_named(&msg)?.len() as u32;
|
||||
let permit = self
|
||||
.0
|
||||
.request_buffer_semaphore
|
||||
.acquire_many(msg_size)
|
||||
.await?;
|
||||
|
||||
let node_id = to.into();
|
||||
select! {
|
||||
res = endpoint.call(&node_id, &msg, strat.rs_priority) => Ok(res??),
|
||||
_ = tokio::time::sleep(strat.rs_timeout) => Err(Error::Timeout),
|
||||
res = endpoint.call(&node_id, &msg, strat.rs_priority) => {
|
||||
drop(permit);
|
||||
Ok(res??)
|
||||
}
|
||||
_ = tokio::time::sleep(strat.rs_timeout) => {
|
||||
drop(permit);
|
||||
Err(Error::Timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +174,7 @@ impl RpcHelper {
|
||||
H: EndpointHandler<M>,
|
||||
{
|
||||
let to = self
|
||||
.0
|
||||
.fullmesh
|
||||
.get_peer_list()
|
||||
.iter()
|
||||
@@ -141,8 +183,8 @@ impl RpcHelper {
|
||||
self.call_many(endpoint, &to[..], msg, strat).await
|
||||
}
|
||||
|
||||
/// Make a RPC call to multiple servers, returning either a Vec of responses, or an error if
|
||||
/// strategy could not be respected due to too many errors
|
||||
/// Make a RPC call to multiple servers, returning either a Vec of responses,
|
||||
/// or an error if quorum could not be reached due to too many errors
|
||||
pub async fn try_call_many<M, H, S>(
|
||||
&self,
|
||||
endpoint: &Arc<Endpoint<M, H>>,
|
||||
@@ -156,54 +198,146 @@ impl RpcHelper {
|
||||
S: Send,
|
||||
{
|
||||
let msg = Arc::new(msg);
|
||||
let mut resp_stream = to
|
||||
.to_vec()
|
||||
.into_iter()
|
||||
.map(|to| {
|
||||
let self2 = self.clone();
|
||||
let msg = msg.clone();
|
||||
let endpoint2 = endpoint.clone();
|
||||
async move { self2.call_arc(&endpoint2, to, msg, strategy).await }
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
|
||||
let mut results = vec![];
|
||||
let mut errors = vec![];
|
||||
// Build future for each request
|
||||
// They are not started now: they are added below in a FuturesUnordered
|
||||
// object that will take care of polling them (see below)
|
||||
let requests = to.iter().cloned().map(|to| {
|
||||
let self2 = self.clone();
|
||||
let msg = msg.clone();
|
||||
let endpoint2 = endpoint.clone();
|
||||
(to, async move {
|
||||
self2.call_arc(&endpoint2, to, msg, strategy).await
|
||||
})
|
||||
});
|
||||
let quorum = strategy.rs_quorum.unwrap_or(to.len());
|
||||
|
||||
while let Some(resp) = resp_stream.next().await {
|
||||
match resp {
|
||||
Ok(msg) => {
|
||||
results.push(msg);
|
||||
if results.len() >= quorum {
|
||||
break;
|
||||
// Vectors in which success results and errors will be collected
|
||||
let mut successes = vec![];
|
||||
let mut errors = vec![];
|
||||
|
||||
if strategy.rs_interrupt_after_quorum {
|
||||
// Case 1: once quorum is reached, other requests don't matter.
|
||||
// What we do here is only send the required number of requests
|
||||
// to reach a quorum, priorizing nodes with the lowest latency.
|
||||
// When there are errors, we start new requests to compensate.
|
||||
|
||||
// Retrieve some status variables that we will use to sort requests
|
||||
let peer_list = self.0.fullmesh.get_peer_list();
|
||||
let ring: Arc<Ring> = self.0.ring.borrow().clone();
|
||||
let our_zone = match ring.config.members.get(&self.0.our_node_id) {
|
||||
Some(pc) => &pc.zone,
|
||||
None => "",
|
||||
};
|
||||
|
||||
// Augment requests with some information used to sort them.
|
||||
// The tuples are as follows:
|
||||
// (is another node?, is another zone?, latency, node ID, request future)
|
||||
// We store all of these tuples in a vec that we can sort.
|
||||
// By sorting this vec, we priorize ourself, then nodes in the same zone,
|
||||
// and within a same zone we priorize nodes with the lowest latency.
|
||||
let mut requests = requests
|
||||
.map(|(to, fut)| {
|
||||
let peer_zone = match ring.config.members.get(&to) {
|
||||
Some(pc) => &pc.zone,
|
||||
None => "",
|
||||
};
|
||||
let peer_avg_ping = peer_list
|
||||
.iter()
|
||||
.find(|x| x.id.as_ref() == to.as_slice())
|
||||
.map(|pi| pi.avg_ping)
|
||||
.flatten()
|
||||
.unwrap_or_else(|| Duration::from_secs(1));
|
||||
(
|
||||
to != self.0.our_node_id,
|
||||
peer_zone != our_zone,
|
||||
peer_avg_ping,
|
||||
to,
|
||||
fut,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sort requests by (priorize ourself, priorize same zone, priorize low latency)
|
||||
requests
|
||||
.sort_by_key(|(diffnode, diffzone, ping, _to, _fut)| (*diffnode, *diffzone, *ping));
|
||||
|
||||
// Make an iterator to take requests in their sorted order
|
||||
let mut requests = requests.into_iter();
|
||||
|
||||
// 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)
|
||||
let mut resp_stream = FuturesUnordered::new();
|
||||
|
||||
// Do some requests and collect results
|
||||
'request_loop: while successes.len() < quorum {
|
||||
// If the current set of requests that are running is not enough to possibly
|
||||
// reach quorum, start some new requests.
|
||||
while successes.len() + resp_stream.len() < quorum {
|
||||
if let Some((_, _, _, _to, fut)) = requests.next() {
|
||||
resp_stream.push(fut);
|
||||
} else {
|
||||
// If we have no request to add, we know that we won't ever
|
||||
// reach quorum: bail out now.
|
||||
break 'request_loop;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
errors.push(e);
|
||||
assert!(!resp_stream.is_empty()); // because of loop invariants
|
||||
|
||||
// Wait for one request to terminate
|
||||
match resp_stream.next().await.unwrap() {
|
||||
Ok(msg) => {
|
||||
successes.push(msg);
|
||||
}
|
||||
Err(e) => {
|
||||
errors.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Case 2: all of the requests need to be sent in all cases,
|
||||
// and need to terminate. (this is the case for writes that
|
||||
// must be spread to n nodes)
|
||||
// Just start all the requests in parallel and return as soon
|
||||
// as the quorum is reached.
|
||||
let mut resp_stream = requests
|
||||
.map(|(_, fut)| fut)
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
|
||||
while let Some(resp) = resp_stream.next().await {
|
||||
match resp {
|
||||
Ok(msg) => {
|
||||
successes.push(msg);
|
||||
if successes.len() >= quorum {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
errors.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !resp_stream.is_empty() {
|
||||
// Continue remaining requests in background.
|
||||
// Continue the remaining requests immediately using tokio::spawn
|
||||
// but enqueue a task in the background runner
|
||||
// to ensure that the process won't exit until the requests are done
|
||||
// (if we had just enqueued the resp_stream.collect directly in the background runner,
|
||||
// the requests might have been put on hold in the background runner's queue,
|
||||
// in which case they might timeout or otherwise fail)
|
||||
let wait_finished_fut = tokio::spawn(async move {
|
||||
resp_stream.collect::<Vec<Result<_, _>>>().await;
|
||||
});
|
||||
self.0.background.spawn(wait_finished_fut.map(|_| Ok(())));
|
||||
}
|
||||
}
|
||||
|
||||
if results.len() >= quorum {
|
||||
// Continue requests in background.
|
||||
// Continue the remaining requests immediately using tokio::spawn
|
||||
// but enqueue a task in the background runner
|
||||
// to ensure that the process won't exit until the requests are done
|
||||
// (if we had just enqueued the resp_stream.collect directly in the background runner,
|
||||
// the requests might have been put on hold in the background runner's queue,
|
||||
// in which case they might timeout or otherwise fail)
|
||||
if !strategy.rs_interrupt_after_quorum {
|
||||
let wait_finished_fut = tokio::spawn(async move {
|
||||
resp_stream.collect::<Vec<_>>().await;
|
||||
});
|
||||
self.background.spawn(wait_finished_fut.map(|_| Ok(())));
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
if successes.len() >= quorum {
|
||||
Ok(successes)
|
||||
} else {
|
||||
let errors = errors.iter().map(|e| format!("{}", e)).collect::<Vec<_>>();
|
||||
Err(Error::Quorum(quorum, results.len(), to.len(), errors))
|
||||
Err(Error::Quorum(quorum, successes.len(), to.len(), errors))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
-13
@@ -4,7 +4,7 @@ use std::io::{Read, Write};
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use async_trait::async_trait;
|
||||
@@ -112,6 +112,7 @@ pub struct KnownNodeInfo {
|
||||
pub id: Uuid,
|
||||
pub addr: SocketAddr,
|
||||
pub is_up: bool,
|
||||
pub last_seen_secs_ago: Option<u64>,
|
||||
pub status: NodeStatus,
|
||||
}
|
||||
|
||||
@@ -234,10 +235,7 @@ impl System {
|
||||
node_status: RwLock::new(HashMap::new()),
|
||||
netapp: netapp.clone(),
|
||||
fullmesh: fullmesh.clone(),
|
||||
rpc: RpcHelper {
|
||||
fullmesh,
|
||||
background: background.clone(),
|
||||
},
|
||||
rpc: RpcHelper::new(netapp.id.into(), fullmesh, background.clone(), ring.clone()),
|
||||
system_endpoint,
|
||||
replication_factor,
|
||||
rpc_listen_addr: config.rpc_bind_addr,
|
||||
@@ -354,6 +352,7 @@ impl System {
|
||||
id: n.id.into(),
|
||||
addr: n.addr,
|
||||
is_up: n.is_up(),
|
||||
last_seen_secs_ago: n.last_seen.map(|t| (Instant::now() - t).as_secs()),
|
||||
status: node_status
|
||||
.get(&n.id.into())
|
||||
.cloned()
|
||||
@@ -498,13 +497,7 @@ impl System {
|
||||
}
|
||||
}
|
||||
|
||||
let peer_list = self
|
||||
.fullmesh
|
||||
.get_peer_list()
|
||||
.iter()
|
||||
.map(|n| (n.id.into(), n.addr))
|
||||
.collect::<Vec<_>>();
|
||||
if let Err(e) = self.persist_peer_list.save_async(&peer_list).await {
|
||||
if let Err(e) = self.save_peer_list().await {
|
||||
warn!("Could not save peer list to file: {}", e);
|
||||
}
|
||||
|
||||
@@ -518,6 +511,28 @@ impl System {
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_peer_list(&self) -> Result<(), Error> {
|
||||
// Prepare new peer list to save to file
|
||||
// It is a vec of tuples (node ID as Uuid, node SocketAddr)
|
||||
let mut peer_list = self
|
||||
.fullmesh
|
||||
.get_peer_list()
|
||||
.iter()
|
||||
.map(|n| (n.id.into(), n.addr))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Before doing it, we read the current peer list file (if it exists)
|
||||
// and append it to the list we are about to save,
|
||||
// so that no peer ID gets lost in the process.
|
||||
if let Ok(mut prev_peer_list) = self.persist_peer_list.load_async().await {
|
||||
prev_peer_list.retain(|(id, _ip)| peer_list.iter().all(|(id2, _ip2)| id2 != id));
|
||||
peer_list.extend(prev_peer_list);
|
||||
}
|
||||
|
||||
// Save new peer list to file
|
||||
self.persist_peer_list.save_async(&peer_list).await
|
||||
}
|
||||
|
||||
async fn pull_config(self: Arc<Self>, peer: Uuid) {
|
||||
let resp = self
|
||||
.rpc
|
||||
@@ -541,7 +556,7 @@ impl EndpointHandler<SystemRpc> for System {
|
||||
SystemRpc::Connect(node) => self.handle_connect(node).await,
|
||||
SystemRpc::PullConfig => Ok(self.handle_pull_config()),
|
||||
SystemRpc::AdvertiseStatus(adv) => self.handle_advertise_status(from.into(), adv).await,
|
||||
SystemRpc::AdvertiseConfig(adv) => self.clone().handle_advertise_config(&adv).await,
|
||||
SystemRpc::AdvertiseConfig(adv) => self.clone().handle_advertise_config(adv).await,
|
||||
SystemRpc::GetKnownNodes => Ok(self.handle_get_known_nodes()),
|
||||
_ => Err(Error::BadRpc("Unexpected RPC message".to_string())),
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ where
|
||||
}
|
||||
/// Get a reference to the value assigned to a key
|
||||
pub fn get(&self, k: &K) -> Option<&V> {
|
||||
match self.vals.binary_search_by(|(k2, _, _)| k2.cmp(&k)) {
|
||||
match self.vals.binary_search_by(|(k2, _, _)| k2.cmp(k)) {
|
||||
Ok(i) => Some(&self.vals[i].2),
|
||||
Err(_) => None,
|
||||
}
|
||||
@@ -132,14 +132,14 @@ where
|
||||
{
|
||||
fn merge(&mut self, other: &Self) {
|
||||
for (k, ts2, v2) in other.vals.iter() {
|
||||
match self.vals.binary_search_by(|(k2, _, _)| k2.cmp(&k)) {
|
||||
match self.vals.binary_search_by(|(k2, _, _)| k2.cmp(k)) {
|
||||
Ok(i) => {
|
||||
let (_, ts1, _v1) = &self.vals[i];
|
||||
if ts2 > ts1 {
|
||||
self.vals[i].1 = *ts2;
|
||||
self.vals[i].2 = v2.clone();
|
||||
} else if ts1 == ts2 {
|
||||
self.vals[i].2.merge(&v2);
|
||||
self.vals[i].2.merge(v2);
|
||||
}
|
||||
}
|
||||
Err(i) => {
|
||||
|
||||
@@ -49,7 +49,7 @@ where
|
||||
|
||||
/// Get a reference to the value assigned to a key
|
||||
pub fn get(&self, k: &K) -> Option<&V> {
|
||||
match self.vals.binary_search_by(|(k2, _)| k2.cmp(&k)) {
|
||||
match self.vals.binary_search_by(|(k2, _)| k2.cmp(k)) {
|
||||
Ok(i) => Some(&self.vals[i].1),
|
||||
Err(_) => None,
|
||||
}
|
||||
@@ -76,9 +76,9 @@ where
|
||||
{
|
||||
fn merge(&mut self, other: &Self) {
|
||||
for (k, v2) in other.vals.iter() {
|
||||
match self.vals.binary_search_by(|(k2, _)| k2.cmp(&k)) {
|
||||
match self.vals.binary_search_by(|(k2, _)| k2.cmp(k)) {
|
||||
Ok(i) => {
|
||||
self.vals[i].1.merge(&v2);
|
||||
self.vals[i].1.merge(v2);
|
||||
}
|
||||
Err(i) => {
|
||||
self.vals.insert(i, (k.clone(), v2.clone()));
|
||||
|
||||
+3
-2
@@ -12,6 +12,7 @@ use garage_util::error::*;
|
||||
use garage_rpc::system::System;
|
||||
|
||||
use crate::crdt::Crdt;
|
||||
use crate::gc::GcTodoEntry;
|
||||
use crate::replication::*;
|
||||
use crate::schema::*;
|
||||
|
||||
@@ -54,7 +55,7 @@ where
|
||||
.expect("Unable to open DB Merkle TODO tree");
|
||||
|
||||
let gc_todo = db
|
||||
.open_tree(&format!("{}:gc_todo", name))
|
||||
.open_tree(&format!("{}:gc_todo_v2", name))
|
||||
.expect("Unable to open DB tree");
|
||||
|
||||
Arc::new(Self {
|
||||
@@ -176,7 +177,7 @@ where
|
||||
let pk_hash = Hash::try_from(&tree_key[..32]).unwrap();
|
||||
let nodes = self.replication.write_nodes(&pk_hash);
|
||||
if nodes.first() == Some(&self.system.id) {
|
||||
self.gc_todo.insert(&tree_key, new_bytes_hash.as_slice())?;
|
||||
GcTodoEntry::new(tree_key, new_bytes_hash).save(&self.gc_todo)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+189
-45
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -12,7 +13,8 @@ use futures_util::future::*;
|
||||
use tokio::sync::watch;
|
||||
|
||||
use garage_util::data::*;
|
||||
use garage_util::error::Error;
|
||||
use garage_util::error::*;
|
||||
use garage_util::time::*;
|
||||
|
||||
use garage_rpc::system::System;
|
||||
use garage_rpc::*;
|
||||
@@ -24,7 +26,12 @@ use crate::schema::*;
|
||||
const TABLE_GC_BATCH_SIZE: usize = 1024;
|
||||
const TABLE_GC_RPC_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
pub struct TableGc<F: TableSchema + 'static, R: TableReplication + 'static> {
|
||||
// GC delay for table entries: 1 day (24 hours)
|
||||
// (the delay before the entry is added in the GC todo list
|
||||
// and the moment the garbage collection actually happens)
|
||||
const TABLE_GC_DELAY: Duration = Duration::from_secs(24 * 3600);
|
||||
|
||||
pub(crate) struct TableGc<F: TableSchema + 'static, R: TableReplication + 'static> {
|
||||
system: Arc<System>,
|
||||
data: Arc<TableData<F, R>>,
|
||||
|
||||
@@ -72,63 +79,100 @@ where
|
||||
async fn gc_loop(self: Arc<Self>, mut must_exit: watch::Receiver<bool>) {
|
||||
while !*must_exit.borrow() {
|
||||
match self.gc_loop_iter().await {
|
||||
Ok(true) => {
|
||||
Ok(None) => {
|
||||
// Stuff was done, loop immediately
|
||||
continue;
|
||||
}
|
||||
Ok(false) => {
|
||||
// Nothing was done, sleep for some time (below)
|
||||
Ok(Some(wait_delay)) => {
|
||||
// Nothing was done, wait specified delay.
|
||||
select! {
|
||||
_ = tokio::time::sleep(wait_delay).fuse() => {},
|
||||
_ = must_exit.changed().fuse() => {},
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("({}) Error doing GC: {}", self.data.name, e);
|
||||
}
|
||||
}
|
||||
select! {
|
||||
_ = tokio::time::sleep(Duration::from_secs(10)).fuse() => {},
|
||||
_ = must_exit.changed().fuse() => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn gc_loop_iter(&self) -> Result<bool, Error> {
|
||||
async fn gc_loop_iter(&self) -> Result<Option<Duration>, Error> {
|
||||
let now = now_msec();
|
||||
|
||||
let mut entries = vec![];
|
||||
let mut excluded = vec![];
|
||||
|
||||
for item in self.data.gc_todo.iter() {
|
||||
let (k, vhash) = item?;
|
||||
// List entries in the GC todo list
|
||||
// These entries are put there when a tombstone is inserted in the table
|
||||
// (see update_entry in data.rs)
|
||||
for entry_kv in self.data.gc_todo.iter() {
|
||||
let (k, vhash) = entry_kv?;
|
||||
let mut todo_entry = GcTodoEntry::parse(&k, &vhash);
|
||||
|
||||
if todo_entry.deletion_time() > now {
|
||||
if entries.is_empty() && excluded.is_empty() {
|
||||
// If the earliest entry in the todo list shouldn't yet be processed,
|
||||
// return a duration to wait in the loop
|
||||
return Ok(Some(Duration::from_millis(
|
||||
todo_entry.deletion_time() - now,
|
||||
)));
|
||||
} else {
|
||||
// Otherwise we have some entries to process, do a normal iteration.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let vhash = Hash::try_from(&vhash[..]).unwrap();
|
||||
|
||||
let v_opt = self
|
||||
// Check if the tombstone is still the current value of the entry.
|
||||
// If not, we don't actually want to GC it, and we will remove it
|
||||
// from the gc_todo table later (below).
|
||||
todo_entry.value = self
|
||||
.data
|
||||
.store
|
||||
.get(&k[..])?
|
||||
.filter(|v| blake2sum(&v[..]) == vhash);
|
||||
.filter(|v| blake2sum(&v[..]) == vhash)
|
||||
.map(|v| v.to_vec());
|
||||
|
||||
if let Some(v) = v_opt {
|
||||
entries.push((ByteBuf::from(k.to_vec()), vhash, ByteBuf::from(v.to_vec())));
|
||||
if todo_entry.value.is_some() {
|
||||
entries.push(todo_entry);
|
||||
if entries.len() >= TABLE_GC_BATCH_SIZE {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
excluded.push((k, vhash));
|
||||
excluded.push(todo_entry);
|
||||
}
|
||||
}
|
||||
|
||||
for (k, vhash) in excluded {
|
||||
self.todo_remove_if_equal(&k[..], vhash)?;
|
||||
// Remove from gc_todo entries for tombstones where we have
|
||||
// detected that the current value has changed and
|
||||
// is no longer a tombstone.
|
||||
for entry in excluded {
|
||||
entry.remove_if_equal(&self.data.gc_todo)?;
|
||||
}
|
||||
|
||||
// Remaining in `entries` is the list of entries we want to GC,
|
||||
// and for which they are still currently tombstones in the table.
|
||||
|
||||
if entries.is_empty() {
|
||||
// Nothing to do in this iteration
|
||||
return Ok(false);
|
||||
// Nothing to do in this iteration (no entries present)
|
||||
// Wait for a default delay of 60 seconds
|
||||
return Ok(Some(Duration::from_secs(60)));
|
||||
}
|
||||
|
||||
debug!("({}) GC: doing {} items", self.data.name, entries.len());
|
||||
|
||||
// Split entries to GC by the set of nodes on which they are stored.
|
||||
// Here we call them partitions but they are not exactly
|
||||
// the same as partitions as defined in the ring: those partitions
|
||||
// are defined by the first 8 bits of the hash, but two of these
|
||||
// partitions can be stored on the same set of nodes.
|
||||
// Here we detect when entries are stored on the same set of nodes:
|
||||
// even if they are not in the same 8-bit partition, we can still
|
||||
// handle them together.
|
||||
let mut partitions = HashMap::new();
|
||||
for (k, vhash, v) in entries {
|
||||
let pkh = Hash::try_from(&k[..32]).unwrap();
|
||||
for entry in entries {
|
||||
let pkh = Hash::try_from(&entry.key[..32]).unwrap();
|
||||
let mut nodes = self.data.replication.write_nodes(&pkh);
|
||||
nodes.retain(|x| *x != self.system.id);
|
||||
nodes.sort();
|
||||
@@ -136,9 +180,12 @@ where
|
||||
if !partitions.contains_key(&nodes) {
|
||||
partitions.insert(nodes.clone(), vec![]);
|
||||
}
|
||||
partitions.get_mut(&nodes).unwrap().push((k, vhash, v));
|
||||
partitions.get_mut(&nodes).unwrap().push(entry);
|
||||
}
|
||||
|
||||
// For each set of nodes that contains some items,
|
||||
// ensure they are aware of the tombstone status, and once they
|
||||
// are, instruct them to delete the entries.
|
||||
let resps = join_all(
|
||||
partitions
|
||||
.into_iter()
|
||||
@@ -146,6 +193,8 @@ where
|
||||
)
|
||||
.await;
|
||||
|
||||
// Collect errors and return a single error value even if several
|
||||
// errors occurred.
|
||||
let mut errs = vec![];
|
||||
for resp in resps {
|
||||
if let Err(e) = resp {
|
||||
@@ -154,7 +203,7 @@ where
|
||||
}
|
||||
|
||||
if errs.is_empty() {
|
||||
Ok(true)
|
||||
Ok(None)
|
||||
} else {
|
||||
Err(Error::Message(
|
||||
errs.into_iter()
|
||||
@@ -162,23 +211,42 @@ where
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
))
|
||||
.err_context("in try_send_and_delete in table GC:")
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_send_and_delete(
|
||||
&self,
|
||||
nodes: Vec<Uuid>,
|
||||
items: Vec<(ByteBuf, Hash, ByteBuf)>,
|
||||
mut items: Vec<GcTodoEntry>,
|
||||
) -> Result<(), Error> {
|
||||
let n_items = items.len();
|
||||
|
||||
// Strategy: we first send all of the values to the remote nodes,
|
||||
// to ensure that they are aware of the tombstone state,
|
||||
// and that the previous state was correctly overwritten
|
||||
// (if they have a newer state that overrides the tombstone, that's fine).
|
||||
// Second, once everyone is at least at the tombstone state,
|
||||
// we instruct everyone to delete the tombstone IF that is still their current state.
|
||||
// If they are now at a different state, it means that that state overrides the
|
||||
// tombstone in the CRDT lattice, and it will be propagated back to us at some point
|
||||
// (either just a regular update that hasn't reached us yet, or later when the
|
||||
// table is synced).
|
||||
|
||||
// Here, we store in updates all of the tombstones to send for step 1,
|
||||
// and in deletes the list of keys and hashes of value for step 2.
|
||||
let mut updates = vec![];
|
||||
let mut deletes = vec![];
|
||||
for (k, vhash, v) in items {
|
||||
updates.push(v);
|
||||
deletes.push((k, vhash));
|
||||
for item in items.iter_mut() {
|
||||
updates.push(ByteBuf::from(item.value.take().unwrap()));
|
||||
deletes.push((ByteBuf::from(item.key.clone()), item.value_hash));
|
||||
}
|
||||
|
||||
// Step 1: ensure everyone is at least at tombstone in CRDT lattice
|
||||
// Here the quorum is nodes.len(): we cannot tolerate even a single failure,
|
||||
// otherwise old values before the tombstone might come back in the data.
|
||||
// GC'ing is not a critical function of the system, so it's not a big
|
||||
// deal if we can't do it right now.
|
||||
self.system
|
||||
.rpc
|
||||
.try_call_many(
|
||||
@@ -189,40 +257,43 @@ where
|
||||
.with_quorum(nodes.len())
|
||||
.with_timeout(TABLE_GC_RPC_TIMEOUT),
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.err_context("GC: send tombstones")?;
|
||||
|
||||
info!(
|
||||
"({}) GC: {} items successfully pushed, will try to delete.",
|
||||
self.data.name, n_items
|
||||
);
|
||||
|
||||
// Step 2: delete tombstones everywhere.
|
||||
// Here we also fail if even a single node returns a failure:
|
||||
// it means that the garbage collection wasn't completed and has
|
||||
// to be retried later.
|
||||
self.system
|
||||
.rpc
|
||||
.try_call_many(
|
||||
&self.endpoint,
|
||||
&nodes[..],
|
||||
GcRpc::DeleteIfEqualHash(deletes.clone()),
|
||||
GcRpc::DeleteIfEqualHash(deletes),
|
||||
RequestStrategy::with_priority(PRIO_BACKGROUND)
|
||||
.with_quorum(nodes.len())
|
||||
.with_timeout(TABLE_GC_RPC_TIMEOUT),
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.err_context("GC: remote delete tombstones")?;
|
||||
|
||||
for (k, vhash) in deletes {
|
||||
self.data.delete_if_equal_hash(&k[..], vhash)?;
|
||||
self.todo_remove_if_equal(&k[..], vhash)?;
|
||||
// GC has been successfull for all of these entries.
|
||||
// We now remove them all from our local table and from the GC todo list.
|
||||
for item in items {
|
||||
self.data
|
||||
.delete_if_equal_hash(&item.key[..], item.value_hash)
|
||||
.err_context("GC: local delete tombstones")?;
|
||||
item.remove_if_equal(&self.data.gc_todo)
|
||||
.err_context("GC: remove from todo list after successfull GC")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn todo_remove_if_equal(&self, key: &[u8], vhash: Hash) -> Result<(), Error> {
|
||||
let _ = self
|
||||
.data
|
||||
.gc_todo
|
||||
.compare_and_swap::<_, _, Vec<u8>>(key, Some(vhash), None)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -240,7 +311,6 @@ where
|
||||
GcRpc::DeleteIfEqualHash(items) => {
|
||||
for (key, vhash) in items.iter() {
|
||||
self.data.delete_if_equal_hash(&key[..], *vhash)?;
|
||||
self.todo_remove_if_equal(&key[..], *vhash)?;
|
||||
}
|
||||
Ok(GcRpc::Ok)
|
||||
}
|
||||
@@ -248,3 +318,77 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An entry stored in the gc_todo Sled tree associated with the table
|
||||
/// Contains helper function for parsing, saving, and removing
|
||||
/// such entry in Sled
|
||||
///
|
||||
/// Format of an entry:
|
||||
/// - key = 8 bytes: timestamp of tombstone
|
||||
/// (used to implement GC delay)
|
||||
/// n bytes: key in the main data table
|
||||
/// - value = hash of the table entry to delete (the tombstone)
|
||||
/// for verification purpose, because we don't want to delete
|
||||
/// things that aren't tombstones
|
||||
pub(crate) struct GcTodoEntry {
|
||||
tombstone_timestamp: u64,
|
||||
key: Vec<u8>,
|
||||
value_hash: Hash,
|
||||
value: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl GcTodoEntry {
|
||||
/// Creates a new GcTodoEntry (not saved in Sled) from its components:
|
||||
/// the key of an entry in the table, and the hash of the associated
|
||||
/// serialized value
|
||||
pub(crate) fn new(key: Vec<u8>, value_hash: Hash) -> Self {
|
||||
Self {
|
||||
tombstone_timestamp: now_msec(),
|
||||
key,
|
||||
value_hash,
|
||||
value: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a GcTodoEntry from a (k, v) pair stored in the gc_todo tree
|
||||
pub(crate) fn parse(sled_k: &[u8], sled_v: &[u8]) -> Self {
|
||||
Self {
|
||||
tombstone_timestamp: u64::from_be_bytes(sled_k[0..8].try_into().unwrap()),
|
||||
key: sled_k[8..].to_vec(),
|
||||
value_hash: Hash::try_from(sled_v).unwrap(),
|
||||
value: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves the GcTodoEntry in the gc_todo tree
|
||||
pub(crate) fn save(&self, gc_todo_tree: &sled::Tree) -> Result<(), Error> {
|
||||
gc_todo_tree.insert(self.todo_table_key(), self.value_hash.as_slice())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes the GcTodoEntry from the gc_todo tree if the
|
||||
/// hash of the serialized value is the same here as in the tree.
|
||||
/// This is usefull to remove a todo entry only under the condition
|
||||
/// that it has not changed since the time it was read, i.e.
|
||||
/// what we have to do is still the same
|
||||
pub(crate) fn remove_if_equal(&self, gc_todo_tree: &sled::Tree) -> Result<(), Error> {
|
||||
let _ = gc_todo_tree.compare_and_swap::<_, _, Vec<u8>>(
|
||||
&self.todo_table_key()[..],
|
||||
Some(self.value_hash),
|
||||
None,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn todo_table_key(&self) -> Vec<u8> {
|
||||
[
|
||||
&u64::to_be_bytes(self.tombstone_timestamp)[..],
|
||||
&self.key[..],
|
||||
]
|
||||
.concat()
|
||||
}
|
||||
|
||||
fn deletion_time(&self) -> u64 {
|
||||
self.tombstone_timestamp + TABLE_GC_DELAY.as_millis() as u64
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -167,7 +167,7 @@ where
|
||||
// Calculate an update to apply to this node
|
||||
// This update is an Option<_>, so that it is None if the update is a no-op
|
||||
// and we can thus skip recalculating and re-storing everything
|
||||
let mutate = match self.read_node_txn(tx, &key)? {
|
||||
let mutate = match self.read_node_txn(tx, key)? {
|
||||
MerkleNode::Empty => new_vhash.map(|vhv| MerkleNode::Leaf(k.to_vec(), vhv)),
|
||||
MerkleNode::Intermediate(mut children) => {
|
||||
let key2 = key.next_key(khash);
|
||||
@@ -270,7 +270,7 @@ where
|
||||
};
|
||||
|
||||
if let Some(new_node) = mutate {
|
||||
let hash = self.put_node_txn(tx, &key, &new_node)?;
|
||||
let hash = self.put_node_txn(tx, key, &new_node)?;
|
||||
Ok(Some(hash))
|
||||
} else {
|
||||
Ok(None)
|
||||
|
||||
@@ -27,7 +27,7 @@ pub struct TableShardedReplication {
|
||||
impl TableReplication for TableShardedReplication {
|
||||
fn read_nodes(&self, hash: &Hash) -> Vec<Uuid> {
|
||||
let ring = self.system.ring.borrow();
|
||||
ring.get_nodes(&hash, self.replication_factor)
|
||||
ring.get_nodes(hash, self.replication_factor)
|
||||
}
|
||||
fn read_quorum(&self) -> usize {
|
||||
self.read_quorum
|
||||
@@ -35,7 +35,7 @@ impl TableReplication for TableShardedReplication {
|
||||
|
||||
fn write_nodes(&self, hash: &Hash) -> Vec<Uuid> {
|
||||
let ring = self.system.ring.borrow();
|
||||
ring.get_nodes(&hash, self.replication_factor)
|
||||
ring.get_nodes(hash, self.replication_factor)
|
||||
}
|
||||
fn write_quorum(&self) -> usize {
|
||||
self.write_quorum
|
||||
|
||||
+2
-2
@@ -266,7 +266,7 @@ where
|
||||
let nodes = self
|
||||
.data
|
||||
.replication
|
||||
.write_nodes(&begin)
|
||||
.write_nodes(begin)
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
if nodes.contains(&self.system.id) {
|
||||
@@ -530,7 +530,7 @@ where
|
||||
Ok(SyncRpc::RootCkDifferent(hash != *h))
|
||||
}
|
||||
SyncRpc::GetNode(k) => {
|
||||
let node = self.merkle.read_node(&k)?;
|
||||
let node = self.merkle.read_node(k)?;
|
||||
Ok(SyncRpc::Node(k.clone(), node))
|
||||
}
|
||||
SyncRpc::Items(items) => {
|
||||
|
||||
+3
-1
@@ -32,6 +32,8 @@ toml = "0.5"
|
||||
futures = "0.3"
|
||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
||||
|
||||
netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
|
||||
#netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
|
||||
netapp = "0.3.0"
|
||||
|
||||
http = "0.2"
|
||||
hyper = "0.14"
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ pub struct Config {
|
||||
pub rpc_public_addr: Option<SocketAddr>,
|
||||
|
||||
/// Bootstrap peers RPC address
|
||||
#[serde(deserialize_with = "deserialize_vec_addr")]
|
||||
#[serde(deserialize_with = "deserialize_vec_addr", default)]
|
||||
pub bootstrap_peers: Vec<(NodeID, SocketAddr)>,
|
||||
/// Consul host to connect to to discover more peers
|
||||
pub consul_host: Option<String>,
|
||||
|
||||
@@ -41,6 +41,9 @@ pub enum Error {
|
||||
#[error(display = "Tokio join error: {}", _0)]
|
||||
TokioJoin(#[error(source)] tokio::task::JoinError),
|
||||
|
||||
#[error(display = "Tokio semaphore acquire error: {}", _0)]
|
||||
TokioSemAcquire(#[error(source)] tokio::sync::AcquireError),
|
||||
|
||||
#[error(display = "Remote error: {}", _0)]
|
||||
RemoteError(String),
|
||||
|
||||
|
||||
@@ -9,3 +9,5 @@ pub mod data;
|
||||
pub mod error;
|
||||
pub mod persister;
|
||||
pub mod time;
|
||||
pub mod token_bucket;
|
||||
pub mod tranquilizer;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use tokio::time::sleep;
|
||||
|
||||
pub struct TokenBucket {
|
||||
// Replenish rate: number of tokens per second
|
||||
replenish_rate: u64,
|
||||
// Current number of tokens
|
||||
tokens: u64,
|
||||
// Last replenish time
|
||||
last_replenish: Instant,
|
||||
}
|
||||
|
||||
impl TokenBucket {
|
||||
pub fn new(replenish_rate: u64) -> Self {
|
||||
Self {
|
||||
replenish_rate,
|
||||
tokens: 0,
|
||||
last_replenish: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn take(&mut self, tokens: u64) {
|
||||
while self.tokens < tokens {
|
||||
let needed = tokens - self.tokens;
|
||||
let delay = (needed as f64) / (self.replenish_rate as f64);
|
||||
sleep(Duration::from_secs_f64(delay)).await;
|
||||
self.replenish();
|
||||
}
|
||||
self.tokens -= tokens;
|
||||
}
|
||||
|
||||
pub fn replenish(&mut self) {
|
||||
let now = Instant::now();
|
||||
let new_tokens =
|
||||
((now - self.last_replenish).as_secs_f64() * (self.replenish_rate as f64)) as u64;
|
||||
self.tokens += new_tokens;
|
||||
self.last_replenish = now;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use tokio::time::sleep;
|
||||
|
||||
/// A tranquilizer is a helper object that is used to make
|
||||
/// background operations not take up too much time.
|
||||
///
|
||||
/// Background operations are done in a loop that does the following:
|
||||
/// - do one step of the background process
|
||||
/// - tranquilize, i.e. wait some time to not overload the system
|
||||
///
|
||||
/// The tranquilizer observes how long the steps take, and keeps
|
||||
/// in memory a number of observations. The tranquilize operation
|
||||
/// simply sleeps k * avg(observed step times), where k is
|
||||
/// the tranquility factor. For instance with a tranquility of 2,
|
||||
/// the tranquilizer will sleep on average 2 units of time for every
|
||||
/// 1 unit of time spent doing the background task.
|
||||
pub struct Tranquilizer {
|
||||
n_observations: usize,
|
||||
observations: VecDeque<Duration>,
|
||||
sum_observations: Duration,
|
||||
last_step_begin: Instant,
|
||||
}
|
||||
|
||||
impl Tranquilizer {
|
||||
pub fn new(n_observations: usize) -> Self {
|
||||
Self {
|
||||
n_observations,
|
||||
observations: VecDeque::with_capacity(n_observations + 1),
|
||||
sum_observations: Duration::ZERO,
|
||||
last_step_begin: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn tranquilize(&mut self, tranquility: u32) {
|
||||
let observation = Instant::now() - self.last_step_begin;
|
||||
|
||||
self.observations.push_back(observation);
|
||||
self.sum_observations += observation;
|
||||
|
||||
while self.observations.len() > self.n_observations {
|
||||
self.sum_observations -= self.observations.pop_front().unwrap();
|
||||
}
|
||||
|
||||
if !self.observations.is_empty() {
|
||||
let delay = (tranquility * self.sum_observations) / (self.observations.len() as u32);
|
||||
sleep(delay).await;
|
||||
}
|
||||
|
||||
self.reset();
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.last_step_begin = Instant::now();
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ pub async fn run_web_server(
|
||||
}
|
||||
});
|
||||
|
||||
let server = Server::bind(&addr).serve(service);
|
||||
let server = Server::bind(addr).serve(service);
|
||||
let graceful = server.with_graceful_shutdown(shutdown_signal);
|
||||
info!("Web server listening on http://{}", addr);
|
||||
|
||||
@@ -95,12 +95,12 @@ async fn serve_file(garage: Arc<Garage>, req: Request<Body>) -> Result<Response<
|
||||
// Get path
|
||||
let path = req.uri().path().to_string();
|
||||
let index = &garage.config.s3_web.index;
|
||||
let key = path_to_key(&path, &index)?;
|
||||
let key = path_to_key(&path, index)?;
|
||||
|
||||
info!("Selected bucket: \"{}\", selected key: \"{}\"", bucket, key);
|
||||
|
||||
let res = match *req.method() {
|
||||
Method::HEAD => handle_head(garage, &req, &bucket, &key).await?,
|
||||
Method::HEAD => handle_head(garage, &req, bucket, &key).await?,
|
||||
Method::GET => handle_get(garage, &req, bucket, &key).await?,
|
||||
_ => return Err(Error::BadRequest("HTTP method not supported".to_string())),
|
||||
};
|
||||
@@ -173,7 +173,7 @@ fn host_to_bucket<'a>(host: &'a str, root: &str) -> &'a str {
|
||||
/// When a path ends with "/", we append the index name to match traditional web server behavior
|
||||
/// which is also AWS S3 behavior.
|
||||
fn path_to_key<'a>(path: &'a str, index: &str) -> Result<Cow<'a, str>, Error> {
|
||||
let path_utf8 = percent_encoding::percent_decode_str(&path).decode_utf8()?;
|
||||
let path_utf8 = percent_encoding::percent_decode_str(path).decode_utf8()?;
|
||||
|
||||
if !path_utf8.starts_with('/') {
|
||||
return Err(Error::BadRequest(
|
||||
|
||||
Reference in New Issue
Block a user