mirror of
https://github.com/deuxfleurs-org/garage.git
synced 2026-08-13 15:46:53 +00:00
Compare commits
122 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a901f7aab | |||
| ded444f6c9 | |||
| 357b72f4ff | |||
| 2c312e9cbd | |||
| 1f7b050b7d | |||
| 56592e1853 | |||
| fbd32933ea | |||
| 5d4b6f2173 | |||
| 4fba06d62e | |||
| 1d0a610690 | |||
| f6aebefcc9 | |||
| 89b8087ba8 | |||
| 76f42a1a2b | |||
| 82600acf77 | |||
| e46dc2a8ef | |||
| 80fdbfb0aa | |||
| ab722cb40f | |||
| 38be811b1c | |||
| 44733474bb | |||
| 07febd3ecd | |||
| 11bdc971e2 | |||
| ff30891999 | |||
| 28a4af73ca | |||
| b823151a0b | |||
| 309d7aef3f | |||
| f91fab8582 | |||
| 7f54706b95 | |||
| d9d199a6c9 | |||
| 03c40a0b24 | |||
| ceb1f0229a | |||
| f310fce34b | |||
| 06df301de5 | |||
| 8adc654713 | |||
| 107853334b | |||
| 1449204439 | |||
| 2e00809af5 | |||
| 2559f63e9b | |||
| 28d86e7602 | |||
| db61f41030 | |||
| 907054775d | |||
| 6b958979bd | |||
| d23b3a14fc | |||
| 4024822585 | |||
| c2cc08852b | |||
| 6f02c36a89 | |||
| 0f5689c169 | |||
| 1e92e9f782 | |||
| 431dee050f | |||
| 2c2b93acdf | |||
| bbb970965c | |||
| b886c75450 | |||
| 48ffaaadfc | |||
| 7de53a4d66 | |||
| 8d77a76df1 | |||
| 454d8474ef | |||
| ed7796924b | |||
| ea36b9ff90 | |||
| e7af006c1c | |||
| db72812f01 | |||
| 729a910e14 | |||
| 9f5433db82 | |||
| fd8074ad9b | |||
| 07e6bcde85 | |||
| 0009fd136c | |||
| 7511ba5530 | |||
| a6e40b75ea | |||
| e1751c8a9c | |||
| 5d4b937a00 | |||
| 5e8baa433d | |||
| 47be652a1f | |||
| 943d76c583 | |||
| 6226f5ceca | |||
| 13b5f28c7e | |||
| 1ef87ac4cb | |||
| 99b532b85b | |||
| e648bf7b69 | |||
| df094bd807 | |||
| f3bf34b6a1 | |||
| bc977f9a7a | |||
| 4b726b0941 | |||
| 70231d68b2 | |||
| e598231ca4 | |||
| c9bc9d89de | |||
| eb97e13a6a | |||
| efbca67ce4 | |||
| 44cd98d2e4 | |||
| dd5304f6fc | |||
| 322dafc761 | |||
| 5d065b8a0f | |||
| 52749e28f7 | |||
| 4da67b0035 | |||
| 1921f4f7e6 | |||
| ebc20a8798 | |||
| 532eca7ff9 | |||
| 2c7bae935a | |||
| 8cd02639dc | |||
| e935861854 | |||
| f0ee3056d3 | |||
| 126b037307 | |||
| 33750c04ed | |||
| 68087ee13d | |||
| 605a630333 | |||
| a35d4da721 | |||
| 8e7e680afe | |||
| 16f6a1a65d | |||
| ad35b18bb1 | |||
| 49154a78d8 | |||
| ff4771c36a | |||
| 381eb9a5a1 | |||
| 2cad656a03 | |||
| 0176da3ad2 | |||
| 40150527b8 | |||
| 2f111e6b3d | |||
| 1b2e1296eb | |||
| a184f0d0b5 | |||
| fcb04843f7 | |||
| 5fb8584247 | |||
| 96561c48a1 | |||
| a49d0ea19f | |||
| 9c9e483375 | |||
| 76cb34a0ae | |||
| ac03fa7937 |
+70
-274
@@ -2,68 +2,26 @@
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
workspace:
|
||||
base: /drone/garage
|
||||
|
||||
volumes:
|
||||
- name: nix_store
|
||||
host:
|
||||
path: /var/lib/drone/nix
|
||||
- name: nix_config
|
||||
temp: {}
|
||||
|
||||
environment:
|
||||
HOME: /drone/garage
|
||||
node:
|
||||
nix-daemon: 1
|
||||
|
||||
steps:
|
||||
- name: setup nix
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
- name: check formatting
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
commands:
|
||||
- cp nix/nix.conf /etc/nix/nix.conf
|
||||
- nix-build --no-build-output --no-out-link shell.nix --arg release false -A inputDerivation
|
||||
|
||||
- name: code quality
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- nix-shell --arg release false --run "cargo fmt -- --check"
|
||||
- nix-shell --arg release false --run "cargo clippy -- --deny warnings"
|
||||
- nix-shell --attr rust --run "cargo fmt -- --check"
|
||||
|
||||
- name: build
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
commands:
|
||||
- nix-build --no-build-output --option log-lines 100 --argstr target x86_64-unknown-linux-musl --arg release false --argstr git_version $DRONE_COMMIT
|
||||
- nix-build --no-build-output --attr clippy.amd64 --argstr git_version ${DRONE_TAG:-$DRONE_COMMIT}
|
||||
|
||||
- name: unit + func tests
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
environment:
|
||||
GARAGE_TEST_INTEGRATION_EXE: result/bin/garage
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- |
|
||||
nix-build \
|
||||
--no-build-output \
|
||||
--option log-lines 100 \
|
||||
--argstr target x86_64-unknown-linux-musl \
|
||||
--argstr compileMode test
|
||||
- nix-build --no-build-output --attr test.amd64
|
||||
- ./result/bin/garage_api-*
|
||||
- ./result/bin/garage_model-*
|
||||
- ./result/bin/garage_rpc-*
|
||||
@@ -73,16 +31,11 @@ steps:
|
||||
- ./result/bin/garage-*
|
||||
- ./result/bin/integration-*
|
||||
|
||||
- name: smoke-test
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
- name: integration tests
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
commands:
|
||||
- nix-build --no-build-output --argstr target x86_64-unknown-linux-musl --arg release false --argstr git_version $DRONE_COMMIT
|
||||
- nix-shell --arg release false --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||
- nix-build --no-build-output --attr clippy.amd64 --argstr git_version ${DRONE_TAG:-$DRONE_COMMIT}
|
||||
- nix-shell --attr integration --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||
|
||||
trigger:
|
||||
event:
|
||||
@@ -92,78 +45,39 @@ trigger:
|
||||
- tag
|
||||
- cron
|
||||
|
||||
node:
|
||||
nix: 1
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: release-linux-x86_64
|
||||
name: release-linux-amd64
|
||||
|
||||
volumes:
|
||||
- name: nix_store
|
||||
host:
|
||||
path: /var/lib/drone/nix
|
||||
- name: nix_config
|
||||
temp: {}
|
||||
|
||||
environment:
|
||||
TARGET: x86_64-unknown-linux-musl
|
||||
node:
|
||||
nix-daemon: 1
|
||||
|
||||
steps:
|
||||
- name: setup nix
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- cp nix/nix.conf /etc/nix/nix.conf
|
||||
- nix-build --no-build-output --no-out-link shell.nix -A inputDerivation
|
||||
|
||||
- name: build
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
commands:
|
||||
- nix-build --no-build-output --argstr target $TARGET --arg release true --argstr git_version $DRONE_COMMIT
|
||||
- nix-build --no-build-output --attr pkgs.amd64.release --argstr git_version ${DRONE_TAG:-$DRONE_COMMIT}
|
||||
- nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage"
|
||||
|
||||
- name: integration
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
commands:
|
||||
- nix-shell --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||
- nix-shell --attr integration --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||
|
||||
- name: push static binary
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: garagehq_aws_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: garagehq_aws_secret_access_key
|
||||
TARGET: "x86_64-unknown-linux-musl"
|
||||
commands:
|
||||
- nix-shell --arg rust false --arg integration false --run "to_s3"
|
||||
- nix-shell --attr release --run "to_s3"
|
||||
|
||||
- name: docker build and publish
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
environment:
|
||||
DOCKER_AUTH:
|
||||
from_secret: docker_auth
|
||||
@@ -174,7 +88,7 @@ steps:
|
||||
- mkdir -p /kaniko/.docker
|
||||
- echo $DOCKER_AUTH > /kaniko/.docker/config.json
|
||||
- export CONTAINER_TAG=${DRONE_TAG:-$DRONE_COMMIT}
|
||||
- nix-shell --arg rust false --arg integration false --run "to_docker"
|
||||
- nix-shell --attr release --run "to_docker"
|
||||
|
||||
|
||||
trigger:
|
||||
@@ -182,78 +96,39 @@ trigger:
|
||||
- promote
|
||||
- cron
|
||||
|
||||
node:
|
||||
nix: 1
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: release-linux-i686
|
||||
name: release-linux-i386
|
||||
|
||||
volumes:
|
||||
- name: nix_store
|
||||
host:
|
||||
path: /var/lib/drone/nix
|
||||
- name: nix_config
|
||||
temp: {}
|
||||
|
||||
environment:
|
||||
TARGET: i686-unknown-linux-musl
|
||||
node:
|
||||
nix-daemon: 1
|
||||
|
||||
steps:
|
||||
- name: setup nix
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- cp nix/nix.conf /etc/nix/nix.conf
|
||||
- nix-build --no-build-output --no-out-link shell.nix -A inputDerivation
|
||||
|
||||
- name: build
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
commands:
|
||||
- nix-build --no-build-output --argstr target $TARGET --arg release true --argstr git_version $DRONE_COMMIT
|
||||
- nix-build --no-build-output --attr pkgs.i386.release --argstr git_version ${DRONE_TAG:-$DRONE_COMMIT}
|
||||
- nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage"
|
||||
|
||||
- name: integration
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
commands:
|
||||
- nix-shell --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||
- nix-shell --attr integration --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||
|
||||
- name: push static binary
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: garagehq_aws_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: garagehq_aws_secret_access_key
|
||||
TARGET: "i686-unknown-linux-musl"
|
||||
commands:
|
||||
- nix-shell --arg rust false --arg integration false --run "to_s3"
|
||||
- nix-shell --attr release --run "to_s3"
|
||||
|
||||
- name: docker build and publish
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
environment:
|
||||
DOCKER_AUTH:
|
||||
from_secret: docker_auth
|
||||
@@ -264,75 +139,41 @@ steps:
|
||||
- mkdir -p /kaniko/.docker
|
||||
- echo $DOCKER_AUTH > /kaniko/.docker/config.json
|
||||
- export CONTAINER_TAG=${DRONE_TAG:-$DRONE_COMMIT}
|
||||
- nix-shell --arg rust false --arg integration false --run "to_docker"
|
||||
- nix-shell --attr release --run "to_docker"
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- promote
|
||||
- cron
|
||||
|
||||
node:
|
||||
nix: 1
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: release-linux-aarch64
|
||||
name: release-linux-arm64
|
||||
|
||||
volumes:
|
||||
- name: nix_store
|
||||
host:
|
||||
path: /var/lib/drone/nix
|
||||
- name: nix_config
|
||||
temp: {}
|
||||
|
||||
environment:
|
||||
TARGET: aarch64-unknown-linux-musl
|
||||
node:
|
||||
nix-daemon: 1
|
||||
|
||||
steps:
|
||||
- name: setup nix
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- cp nix/nix.conf /etc/nix/nix.conf
|
||||
- nix-build --no-build-output --no-out-link ./shell.nix --arg rust false --arg integration false -A inputDerivation
|
||||
|
||||
- name: build
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
commands:
|
||||
- nix-build --no-build-output --argstr target $TARGET --arg release true --argstr git_version $DRONE_COMMIT
|
||||
- nix-build --no-build-output --attr pkgs.arm64.release --argstr git_version ${DRONE_TAG:-$DRONE_COMMIT}
|
||||
- nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage"
|
||||
|
||||
- name: push static binary
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: garagehq_aws_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: garagehq_aws_secret_access_key
|
||||
TARGET: "aarch64-unknown-linux-musl"
|
||||
commands:
|
||||
- nix-shell --arg rust false --arg integration false --run "to_s3"
|
||||
- nix-shell --attr release --run "to_s3"
|
||||
|
||||
- name: docker build and publish
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
environment:
|
||||
DOCKER_AUTH:
|
||||
from_secret: docker_auth
|
||||
@@ -343,75 +184,41 @@ steps:
|
||||
- mkdir -p /kaniko/.docker
|
||||
- echo $DOCKER_AUTH > /kaniko/.docker/config.json
|
||||
- export CONTAINER_TAG=${DRONE_TAG:-$DRONE_COMMIT}
|
||||
- nix-shell --arg rust false --arg integration false --run "to_docker"
|
||||
- nix-shell --attr release --run "to_docker"
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- promote
|
||||
- cron
|
||||
|
||||
node:
|
||||
nix: 1
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: release-linux-armv6l
|
||||
name: release-linux-arm
|
||||
|
||||
volumes:
|
||||
- name: nix_store
|
||||
host:
|
||||
path: /var/lib/drone/nix
|
||||
- name: nix_config
|
||||
temp: {}
|
||||
|
||||
environment:
|
||||
TARGET: armv6l-unknown-linux-musleabihf
|
||||
node:
|
||||
nix-daemon: 1
|
||||
|
||||
steps:
|
||||
- name: setup nix
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
commands:
|
||||
- cp nix/nix.conf /etc/nix/nix.conf
|
||||
- nix-build --no-build-output --no-out-link --arg rust false --arg integration false -A inputDerivation
|
||||
|
||||
- name: build
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
commands:
|
||||
- nix-build --no-build-output --argstr target $TARGET --arg release true --argstr git_version $DRONE_COMMIT
|
||||
- nix-build --no-build-output --attr pkgs.arm.release --argstr git_version ${DRONE_TAG:-$DRONE_COMMIT}
|
||||
- nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage"
|
||||
|
||||
- name: push static binary
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: garagehq_aws_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: garagehq_aws_secret_access_key
|
||||
TARGET: "armv6l-unknown-linux-musleabihf"
|
||||
commands:
|
||||
- nix-shell --arg integration false --arg rust false --run "to_s3"
|
||||
- nix-shell --attr release --run "to_s3"
|
||||
|
||||
- name: docker build and publish
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
- name: nix_config
|
||||
path: /etc/nix
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
environment:
|
||||
DOCKER_AUTH:
|
||||
from_secret: docker_auth
|
||||
@@ -422,32 +229,24 @@ steps:
|
||||
- mkdir -p /kaniko/.docker
|
||||
- echo $DOCKER_AUTH > /kaniko/.docker/config.json
|
||||
- export CONTAINER_TAG=${DRONE_TAG:-$DRONE_COMMIT}
|
||||
- nix-shell --arg rust false --arg integration false --run "to_docker"
|
||||
- nix-shell --attr release --run "to_docker"
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- promote
|
||||
- cron
|
||||
|
||||
node:
|
||||
nix: 1
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: refresh-release-page
|
||||
|
||||
volumes:
|
||||
- name: nix_store
|
||||
host:
|
||||
path: /var/lib/drone/nix
|
||||
node:
|
||||
nix-daemon: 1
|
||||
|
||||
steps:
|
||||
- name: refresh-index
|
||||
image: nixpkgs/nix:nixos-21.05
|
||||
volumes:
|
||||
- name: nix_store
|
||||
path: /nix
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: garagehq_aws_access_key_id
|
||||
@@ -455,24 +254,21 @@ steps:
|
||||
from_secret: garagehq_aws_secret_access_key
|
||||
commands:
|
||||
- mkdir -p /etc/nix && cp nix/nix.conf /etc/nix/nix.conf
|
||||
- nix-shell --arg integration false --arg rust false --run "refresh_index"
|
||||
- nix-shell --attr release --run "refresh_index"
|
||||
|
||||
depends_on:
|
||||
- release-linux-x86_64
|
||||
- release-linux-i686
|
||||
- release-linux-aarch64
|
||||
- release-linux-armv6l
|
||||
- release-linux-amd64
|
||||
- release-linux-i386
|
||||
- release-linux-arm64
|
||||
- release-linux-arm
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- promote
|
||||
- cron
|
||||
|
||||
node:
|
||||
nix: 1
|
||||
|
||||
---
|
||||
kind: signature
|
||||
hmac: 3fc19d6f9a3555519c8405e3281b2e74289bb802f644740d5481d53df3a01fa4
|
||||
hmac: 362639b4c9541ad9bd06ff7f72b5235b2b0216bcb16eececd25285b6fe94ba6f
|
||||
|
||||
...
|
||||
|
||||
Generated
+262
-287
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"src/db",
|
||||
"src/util",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.PHONY: doc all release shell run1 run2 run3
|
||||
|
||||
all:
|
||||
clear; cargo build --all-features
|
||||
clear; cargo build
|
||||
|
||||
release:
|
||||
nix-build --arg release true
|
||||
|
||||
+22
-136
@@ -1,147 +1,33 @@
|
||||
{
|
||||
system ? builtins.currentSystem,
|
||||
release ? false,
|
||||
target ? "x86_64-unknown-linux-musl",
|
||||
compileMode ? null,
|
||||
git_version ? null,
|
||||
}:
|
||||
|
||||
with import ./nix/common.nix;
|
||||
|
||||
let
|
||||
crossSystem = { config = target; };
|
||||
in let
|
||||
log = v: builtins.trace v v;
|
||||
let
|
||||
pkgs = import pkgsSrc { };
|
||||
compile = import ./nix/compile.nix;
|
||||
build_debug_and_release = (target: {
|
||||
debug = (compile { inherit target git_version; release = false; }).workspace.garage { compileMode = "build"; };
|
||||
release = (compile { inherit target git_version; release = true; }).workspace.garage { compileMode = "build"; };
|
||||
});
|
||||
test = (rustPkgs: pkgs.symlinkJoin {
|
||||
name ="garage-tests";
|
||||
paths = builtins.map (key: rustPkgs.workspace.${key} { compileMode = "test"; }) (builtins.attrNames rustPkgs.workspace);
|
||||
});
|
||||
|
||||
pkgs = import pkgsSrc {
|
||||
inherit system crossSystem;
|
||||
overlays = [ cargo2nixOverlay ];
|
||||
in {
|
||||
pkgs = {
|
||||
amd64 = build_debug_and_release "x86_64-unknown-linux-musl";
|
||||
i386 = build_debug_and_release "i686-unknown-linux-musl";
|
||||
arm64 = build_debug_and_release "aarch64-unknown-linux-musl";
|
||||
arm = build_debug_and_release "armv6l-unknown-linux-musleabihf";
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
Rust and Nix triples are not the same. Cargo2nix has a dedicated library
|
||||
to convert Nix triples to Rust ones. We need this conversion as we want to
|
||||
set later options linked to our (rust) target in a generic way. Not only
|
||||
the triple terminology is different, but also the "roles" are named differently.
|
||||
Nix uses a build/host/target terminology where Nix's "host" maps to Cargo's "target".
|
||||
*/
|
||||
rustTarget = log (pkgs.rustBuilder.rustLib.rustTriple pkgs.stdenv.hostPlatform);
|
||||
|
||||
/*
|
||||
Cargo2nix is built for rustOverlay which installs Rust from Mozilla releases.
|
||||
We want our own Rust to avoid incompatibilities, like we had with musl 1.2.0.
|
||||
rustc was built with musl < 1.2.0 and nix shipped musl >= 1.2.0 which lead to compilation breakage.
|
||||
So we want a Rust release that is bound to our Nix repository to avoid these problems.
|
||||
See here for more info: https://musl.libc.org/time64.html
|
||||
Because Cargo2nix does not support the Rust environment shipped by NixOS,
|
||||
we emulate the structure of the Rust object created by rustOverlay.
|
||||
In practise, rustOverlay ships rustc+cargo in a single derivation while
|
||||
NixOS ships them in separate ones. We reunite them with symlinkJoin.
|
||||
*/
|
||||
rustChannel = pkgs.symlinkJoin {
|
||||
name ="rust-channel";
|
||||
paths = [
|
||||
pkgs.rustPlatform.rust.rustc
|
||||
pkgs.rustPlatform.rust.cargo
|
||||
];
|
||||
test = {
|
||||
amd64 = test (compile { inherit git_version; target = "x86_64-unknown-linux-musl"; });
|
||||
};
|
||||
|
||||
/*
|
||||
Cargo2nix provides many overrides by default, you can take inspiration from them:
|
||||
https://github.com/cargo2nix/cargo2nix/blob/master/overlay/overrides.nix
|
||||
|
||||
You can have a complete list of the available options by looking at the overriden object, mkcrate:
|
||||
https://github.com/cargo2nix/cargo2nix/blob/master/overlay/mkcrate.nix
|
||||
*/
|
||||
overrides = pkgs.rustBuilder.overrides.all ++ [
|
||||
/*
|
||||
[1] We need to alter Nix hardening to be able to statically compile: PIE,
|
||||
Position Independent Executables seems to be supported only on amd64. Having
|
||||
this flags set either make our executables crash or compile as dynamic on many platforms.
|
||||
In the following section codegenOpts, we reactive it for the supported targets
|
||||
(only amd64 curently) through the `-static-pie` flag. PIE is a feature used
|
||||
by ASLR, which helps mitigate security issues.
|
||||
Learn more about Nix Hardening: https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/cc-wrapper/add-hardening.sh
|
||||
|
||||
[2] We want to inject the git version while keeping the build deterministic.
|
||||
As we do not want to consider the .git folder as part of the input source,
|
||||
we ask the user (the CI often) to pass the value to Nix.
|
||||
*/
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "garage_rpc";
|
||||
overrideAttrs = drv:
|
||||
/* [1] */ { hardeningDisable = [ "pie" ]; }
|
||||
//
|
||||
/* [2] */ (if git_version != null then {
|
||||
preConfigure = ''
|
||||
${drv.preConfigure or ""}
|
||||
export GIT_VERSION="${git_version}"
|
||||
'';
|
||||
} else {});
|
||||
})
|
||||
|
||||
/*
|
||||
We ship some parts of the code disabled by default by putting them behind a flag.
|
||||
It speeds up the compilation (when the feature is not required) and released crates have less dependency by default (less attack surface, disk space, etc.).
|
||||
But we want to ship these additional features when we release Garage.
|
||||
In the end, we chose to exclude all features from debug builds while putting (all of) them in the release builds.
|
||||
Currently, the only feature of Garage is kubernetes-discovery from the garage_rpc crate.
|
||||
*/
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "garage_rpc";
|
||||
overrideArgs = old:
|
||||
{
|
||||
features = if release then [ "kubernetes-discovery" ] else [];
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
packageFun = import ./Cargo.nix;
|
||||
|
||||
/*
|
||||
We compile fully static binaries with musl to simplify deployment on most systems.
|
||||
When possible, we reactivate PIE hardening (see above).
|
||||
|
||||
Also, if you set the RUSTFLAGS environment variable, the following parameters will
|
||||
be ignored.
|
||||
|
||||
For more information on static builds, please refer to Rust's RFC 1721.
|
||||
https://rust-lang.github.io/rfcs/1721-crt-static.html#specifying-dynamicstatic-c-runtime-linkage
|
||||
*/
|
||||
|
||||
codegenOpts = {
|
||||
"armv6l-unknown-linux-musleabihf" = [ "target-feature=+crt-static" "link-arg=-static" ]; /* compile as dynamic with static-pie */
|
||||
"aarch64-unknown-linux-musl" = [ "target-feature=+crt-static" "link-arg=-static" ]; /* segfault with static-pie */
|
||||
"i686-unknown-linux-musl" = [ "target-feature=+crt-static" "link-arg=-static" ]; /* segfault with static-pie */
|
||||
"x86_64-unknown-linux-musl" = [ "target-feature=+crt-static" "link-arg=-static-pie" ];
|
||||
clippy = {
|
||||
amd64 = (compile { inherit git_version; compiler = "clippy"; }).workspace.garage { compileMode = "build"; } ;
|
||||
};
|
||||
|
||||
/*
|
||||
The following definition is not elegant as we use a low level function of Cargo2nix
|
||||
that enables us to pass our custom rustChannel object. We need this low level definition
|
||||
to pass Nix's Rust toolchains instead of Mozilla's one.
|
||||
|
||||
target is mandatory but must be kept to null to allow cargo2nix to set it to the appropriate value
|
||||
for each crate.
|
||||
*/
|
||||
rustPkgs = pkgs.rustBuilder.makePackageSet {
|
||||
inherit packageFun rustChannel release codegenOpts;
|
||||
packageOverrides = overrides;
|
||||
target = null;
|
||||
|
||||
buildRustPackages = pkgs.buildPackages.rustBuilder.makePackageSet {
|
||||
inherit rustChannel packageFun codegenOpts;
|
||||
packageOverrides = overrides;
|
||||
target = null;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
in
|
||||
if compileMode == "test"
|
||||
then pkgs.symlinkJoin {
|
||||
name ="garage-tests";
|
||||
paths = builtins.map (key: rustPkgs.workspace.${key} { inherit compileMode; }) (builtins.attrNames rustPkgs.workspace);
|
||||
}
|
||||
else rustPkgs.workspace.garage { inherit compileMode; }
|
||||
}
|
||||
|
||||
@@ -5,12 +5,14 @@ weight = 25
|
||||
|
||||
## Configuring a bucket for website access
|
||||
|
||||
There are two methods to expose buckets as website:
|
||||
There are three methods to expose buckets as website:
|
||||
|
||||
1. using the PutBucketWebsite S3 API call, which is allowed for access keys that have the owner permission bit set
|
||||
|
||||
2. from the Garage CLI, by an adminstrator of the cluster
|
||||
|
||||
3. using the Garage administration API
|
||||
|
||||
The `PutBucketWebsite` API endpoint [is documented](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketWebsite.html) in the official AWS docs.
|
||||
This endpoint can also be called [using `aws s3api`](https://docs.aws.amazon.com/cli/latest/reference/s3api/put-bucket-website.html) on the command line.
|
||||
The website configuration supported by Garage is only a subset of the possibilities on Amazon S3: redirections are not supported, only the index document and error document can be specified.
|
||||
|
||||
@@ -20,40 +20,76 @@ sudo apt-get update
|
||||
sudo apt-get install build-essential
|
||||
```
|
||||
|
||||
## Using source from `crates.io`
|
||||
|
||||
Garage's source code is published on `crates.io`, Rust's official package repository.
|
||||
This means you can simply ask `cargo` to download and build this source code for you:
|
||||
|
||||
```bash
|
||||
cargo install garage
|
||||
```
|
||||
|
||||
That's all, `garage` should be in `$HOME/.cargo/bin`.
|
||||
|
||||
You can add this folder to your `$PATH` or copy the binary somewhere else on your system.
|
||||
For instance:
|
||||
|
||||
```bash
|
||||
sudo cp $HOME/.cargo/bin/garage /usr/local/bin/garage
|
||||
```
|
||||
|
||||
|
||||
## Using source from the Gitea repository
|
||||
## Building from source from the Gitea repository
|
||||
|
||||
The primary location for Garage's source code is the
|
||||
[Gitea repository](https://git.deuxfleurs.fr/Deuxfleurs/garage).
|
||||
[Gitea repository](https://git.deuxfleurs.fr/Deuxfleurs/garage),
|
||||
which contains all of the released versions as well as the code
|
||||
for the developpement of the next version.
|
||||
|
||||
Clone the repository and build Garage with the following commands:
|
||||
Clone the repository and enter it as follows:
|
||||
|
||||
```bash
|
||||
git clone https://git.deuxfleurs.fr/Deuxfleurs/garage.git
|
||||
cd garage
|
||||
cargo build
|
||||
```
|
||||
|
||||
Be careful, as this will make a debug build of Garage, which will be extremely slow!
|
||||
To make a release build, invoke `cargo build --release` (this takes much longer).
|
||||
If you wish to build a specific version of Garage, check out the corresponding tag. For instance:
|
||||
|
||||
The binaries built this way are found in `target/{debug,release}/garage`.
|
||||
```bash
|
||||
git tag # List available tags
|
||||
git checkout v0.8.0 # Change v0.8.0 with the version you wish to build
|
||||
```
|
||||
|
||||
Otherwise you will be building a developpement build from the `main` branch
|
||||
that includes all of the changes to be released in the next version.
|
||||
Be careful that such a build might be unstable or contain bugs,
|
||||
and could be incompatible with nodes that run stable versions of Garage.
|
||||
|
||||
Finally, build Garage with the following command:
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
The binary built this way can now be found in `target/release/garage`.
|
||||
You may simply copy this binary to somewhere in your `$PATH` in order to
|
||||
have the `garage` command available in your shell, for instance:
|
||||
|
||||
```bash
|
||||
sudo cp target/release/garage /usr/local/bin/garage
|
||||
```
|
||||
|
||||
If you are planning to develop Garage,
|
||||
you might be interested in producing debug builds, which compile faster but run slower:
|
||||
this can be done by removing the `--release` flag, and the resulting build can then
|
||||
be found in `target/debug/garage`.
|
||||
|
||||
## List of available Cargo feature flags
|
||||
|
||||
Garage supports a number of compilation options in the form of Cargo feature flags,
|
||||
which can be used to provide builds adapted to your system and your use case.
|
||||
To produce a build with a given set of features, invoke the `cargo build` command
|
||||
as follows:
|
||||
|
||||
```bash
|
||||
# This will build the default feature set plus feature1, feature2 and feature3
|
||||
cargo build --release --features feature1,feature2,feature3
|
||||
# This will build ONLY feature1, feature2 and feature3
|
||||
cargo build --release --no-default-features \
|
||||
--features feature1,feature2,feature3
|
||||
```
|
||||
|
||||
The following feature flags are available in v0.8.0:
|
||||
|
||||
| Feature flag | Enabled | Description |
|
||||
| ------------ | ------- | ----------- |
|
||||
| `bundled-libs` | *by default* | Use bundled version of sqlite3, zstd, lmdb and libsodium |
|
||||
| `system-libs` | optional | Use system version of sqlite3, zstd, lmdb and libsodium<br>if available (exclusive with `bundled-libs`, build using<br>`cargo build --no-default-features --features system-libs`) |
|
||||
| `k2v` | optional | Enable the experimental K2V API (if used, all nodes on your<br>Garage cluster must have it enabled as well) |
|
||||
| `kubernetes-discovery` | optional | Enable automatic registration and discovery<br>of cluster nodes through the Kubernetes API |
|
||||
| `metrics` | *by default* | Enable collection of metrics in Prometheus format on the admin API |
|
||||
| `telemetry-otlp` | optional | Enable collection of execution traces using OpenTelemetry |
|
||||
| `sled` | *by default* | Enable using Sled to store Garage's metadata |
|
||||
| `lmdb` | optional | Enable using LMDB to store Garage's metadata |
|
||||
| `sqlite` | optional | Enable using Sqlite3 to store Garage's metadata |
|
||||
|
||||
@@ -280,3 +280,25 @@ Traefik's caching middleware is only available on [entreprise version](https://d
|
||||
[http.middlewares]
|
||||
[http.middlewares.gzip_compress.compress]
|
||||
```
|
||||
|
||||
## Caddy
|
||||
|
||||
Your Caddy configuration can be as simple as:
|
||||
|
||||
```caddy
|
||||
s3.garage.tld, *.s3.garage.tld {
|
||||
reverse_proxy localhost:3900 192.168.1.2:3900 example.tld:3900
|
||||
}
|
||||
|
||||
*.web.garage.tld {
|
||||
reverse_proxy localhost:3902 192.168.1.2:3900 example.tld:3900
|
||||
}
|
||||
|
||||
admin.garage.tld {
|
||||
reverse_proxy localhost:3903
|
||||
}
|
||||
```
|
||||
|
||||
But at the same time, the `reverse_proxy` is very flexible.
|
||||
For a production deployment, you should [read its documentation](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy) as it supports features like DNS discovery of upstreams, load balancing with checks, streaming parameters, etc.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
+++
|
||||
title = "Benchmarks"
|
||||
weight = 10
|
||||
weight = 40
|
||||
+++
|
||||
|
||||
With Garage, we wanted to build a software defined storage service that follow the [KISS principle](https://en.wikipedia.org/wiki/KISS_principle),
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
+++
|
||||
title = "Goals and use cases"
|
||||
weight = 5
|
||||
weight = 10
|
||||
+++
|
||||
|
||||
## Goals and non-goals
|
||||
|
||||
Garage is a lightweight geo-distributed data store that implements the
|
||||
[Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html)
|
||||
object storage protocole. It enables applications to store large blobs such
|
||||
object storage protocol. It enables applications to store large blobs such
|
||||
as pictures, video, images, documents, etc., in a redundant multi-node
|
||||
setting. S3 is versatile enough to also be used to publish a static
|
||||
website.
|
||||
|
||||
@@ -20,6 +20,49 @@ In the meantime, you can find some information at the following links:
|
||||
- [an old design draft](@/documentation/working-documents/design-draft.md)
|
||||
|
||||
|
||||
## Request routing logic
|
||||
|
||||
Data retrieval requests to Garage endpoints (S3 API and websites) are resolved
|
||||
to an individual object in a bucket. Since objects are replicated to multiple nodes
|
||||
Garage must ensure consistency before answering the request.
|
||||
|
||||
### Using quorum to ensure consistency
|
||||
|
||||
Garage ensures consistency by attempting to establish a quorum with the
|
||||
data nodes responsible for the object. When a majority of the data nodes
|
||||
have provided metadata on a object Garage can then answer the request.
|
||||
|
||||
When a request arrives Garage will, assuming the recommended 3 replicas, perform the following actions:
|
||||
|
||||
- Make a request to the two preferred nodes for object metadata
|
||||
- Try the third node if one of the two initial requests fail
|
||||
- Check that the metadata from at least 2 nodes match
|
||||
- Check that the object hasn't been marked deleted
|
||||
- Answer the request with inline data from metadata if object is small enough
|
||||
- Or get data blocks from the preferred nodes and answer using the assembled object
|
||||
|
||||
Garage dynamically determines which nodes to query based on health, preference, and
|
||||
which nodes actually host a given data. Garage has no concept of "primary" so any
|
||||
healthy node with the data can be used as long as a quorum is reached for the metadata.
|
||||
|
||||
### Node health
|
||||
|
||||
Garage keeps a TCP session open to each node in the cluster and periodically pings them. If a connection
|
||||
cannot be established, or a node fails to answer a number of pings, the target node is marked as failed.
|
||||
Failed nodes are not used for quorum or other internal requests.
|
||||
|
||||
### Node preference
|
||||
|
||||
Garage prioritizes which nodes to query according to a few criteria:
|
||||
|
||||
- A node always prefers itself if it can answer the request
|
||||
- Then the node prioritizes nodes in the same zone
|
||||
- Finally the nodes with the lowest latency are prioritized
|
||||
|
||||
|
||||
For further reading on the cluster structure look at the [gateway](@/documentation/cookbook/gateways.md)
|
||||
and [cluster layout management](@/documentation/reference-manual/layout.md) pages.
|
||||
|
||||
## Garbage collection
|
||||
|
||||
A faulty garbage collection procedure has been the cause of
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
+++
|
||||
title = "Related work"
|
||||
weight = 15
|
||||
weight = 50
|
||||
+++
|
||||
|
||||
## Context
|
||||
|
||||
@@ -9,6 +9,15 @@ Let's start your Garage journey!
|
||||
In this chapter, we explain how to deploy Garage as a single-node server
|
||||
and how to interact with it.
|
||||
|
||||
## What is Garage?
|
||||
|
||||
Before jumping in, you might be interested in reading the following pages:
|
||||
|
||||
- [Goals and use cases](@/documentation/design/goals.md)
|
||||
- [List of features](@/documentation/reference-manual/features.md)
|
||||
|
||||
## Scope of this tutorial
|
||||
|
||||
Our goal is to introduce you to Garage's workflows.
|
||||
Following this guide is recommended before moving on to
|
||||
[configuring a multi-node cluster](@/documentation/cookbook/real-world.md).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
+++
|
||||
title = "Administration API"
|
||||
weight = 16
|
||||
weight = 60
|
||||
+++
|
||||
|
||||
The Garage administration API is accessible through a dedicated server whose
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
+++
|
||||
title = "Garage CLI"
|
||||
weight = 15
|
||||
weight = 30
|
||||
+++
|
||||
|
||||
The Garage CLI is mostly self-documented. Make use of the `help` subcommand
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
+++
|
||||
title = "Configuration file format"
|
||||
weight = 5
|
||||
weight = 20
|
||||
+++
|
||||
|
||||
Here is an example `garage.toml` configuration file that illustrates all of the possible options:
|
||||
@@ -10,7 +10,6 @@ metadata_dir = "/var/lib/garage/meta"
|
||||
data_dir = "/var/lib/garage/data"
|
||||
|
||||
block_size = 1048576
|
||||
block_manager_background_tranquility = 2
|
||||
|
||||
replication_mode = "3"
|
||||
|
||||
@@ -87,17 +86,6 @@ files will remain available. This however means that chunks from existing files
|
||||
will not be deduplicated with chunks from newly uploaded files, meaning you
|
||||
might use more storage space that is optimally possible.
|
||||
|
||||
### `block_manager_background_tranquility`
|
||||
|
||||
This parameter tunes the activity of the background worker responsible for
|
||||
resyncing data blocks between nodes. The higher the tranquility value is set,
|
||||
the more the background worker will wait between iterations, meaning the load
|
||||
on the system (including network usage between nodes) will be reduced. The
|
||||
minimal value for this parameter is `0`, where the background worker will
|
||||
allways work at maximal throughput to resynchronize blocks. The default value
|
||||
is `2`, where the background worker will try to spend at most 1/3 of its time
|
||||
working, and 2/3 sleeping in order to reduce system load.
|
||||
|
||||
### `replication_mode`
|
||||
|
||||
Garage supports the following replication modes:
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
+++
|
||||
title = "List of Garage features"
|
||||
weight = 10
|
||||
+++
|
||||
|
||||
|
||||
### S3 API
|
||||
|
||||
The main goal of Garage is to provide an object storage service that is compatible with the
|
||||
[S3 API](https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html) from Amazon Web Services.
|
||||
We try to adhere as strictly as possible to the semantics of the API as implemented by Amazon
|
||||
and other vendors such as Minio or CEPH.
|
||||
|
||||
Of course Garage does not implement the full span of API endpoints that AWS S3 does;
|
||||
the exact list of S3 features implemented by Garage can be found [on our S3 compatibility page](@/documentation/reference-manual/s3-compatibility.md).
|
||||
|
||||
### Geo-distribution
|
||||
|
||||
Garage allows you to store copies of your data in multiple geographical locations in order to maximize resilience
|
||||
to adverse events, such as network/power outages or hardware failures.
|
||||
This allows Garage to run very well even at home, using consumer-grade Internet connectivity
|
||||
(such as FTTH) and power, as long as cluster nodes can be spawned at several physical locations.
|
||||
Garage exploits knowledge of the capacity and physical location of each storage node to design
|
||||
a storage plan that best exploits the available storage capacity while satisfying the geo-distributed replication constraint.
|
||||
|
||||
To learn more about geo-distributed Garage clusters,
|
||||
read our documentation on [setting up a real-world deployment](@/documentation/cookbook/real-world.md).
|
||||
|
||||
### Standalone/self-contained
|
||||
|
||||
Garage is extremely simple to deploy, and does not depend on any external service to run.
|
||||
This makes setting up and administering storage clusters, we hope, as easy as it could be.
|
||||
|
||||
### Flexible topology
|
||||
|
||||
A Garage cluster can very easily evolve over time, as storage nodes are added or removed.
|
||||
Garage will automatically rebalance data between nodes as needed to ensure the desired number of copies.
|
||||
Read about cluster layout management [here](@/documentation/reference-manual/layout.md).
|
||||
|
||||
### No RAFT slowing you down
|
||||
|
||||
It might seem strange to tout the absence of something as a desirable feature,
|
||||
but this is in fact a very important point! Garage does not use RAFT or another
|
||||
consensus algorithm internally to order incoming requests: this means that all requests
|
||||
directed to a Garage cluster can be handled independently of one another instead
|
||||
of going through a central bottleneck (the leader node).
|
||||
As a consequence, requests can be handled much faster, even in cases where latency
|
||||
between cluster nodes is important (see our [benchmarks](@/documentation/design/benchmarks/index.md) for data on this).
|
||||
This is particularly usefull when nodes are far from one another and talk to one other through standard Internet connections.
|
||||
|
||||
### Several replication modes
|
||||
|
||||
Garage supports a variety of replication modes, with 1 copy, 2 copies or 3 copies of your data,
|
||||
and with various levels of consistency, in order to adapt to a variety of usage scenarios.
|
||||
Read our reference page on [supported replication modes](@/documentation/reference-manual/configuration.md#replication-mode)
|
||||
to select the replication mode best suited to your use case (hint: in most cases, `replication_mode = "3"` is what you want).
|
||||
|
||||
### Web server for static websites
|
||||
|
||||
A storage bucket can easily be configured to be served directly by Garage as a static web site.
|
||||
Domain names for multiple websites directly map to bucket names, making it easy to build
|
||||
a platform for your users to autonomously build and host their websites over Garage.
|
||||
Surprisingly, none of the other alternative S3 implementations we surveyed (such as Minio
|
||||
or CEPH) support publishing static websites from S3 buckets, a feature that is however
|
||||
directly inherited from S3 on AWS.
|
||||
Read more on our [dedicated documentation page](@/documentation/cookbook/exposing-websites.md).
|
||||
|
||||
### Bucket names as aliases
|
||||
|
||||
In Garage, a bucket may have several names, known as aliases.
|
||||
Aliases can easily be added and removed on demand:
|
||||
this allows to easily rename buckets if needed
|
||||
without having to copy all of their content, something that cannot be done on AWS.
|
||||
For buckets served as static websites, having multiple aliases for a bucket can allow
|
||||
exposing the same content under different domain names.
|
||||
|
||||
Garage also supports bucket aliases which are local to a single user:
|
||||
this allows different users to have different buckets with the same name, thus avoiding naming collisions.
|
||||
This can be helpfull for instance if you want to write an application that creates per-user buckets with always the same name.
|
||||
|
||||
This feature is totally invisible to S3 clients and does not break compatibility with AWS.
|
||||
|
||||
### Cluster administration API
|
||||
|
||||
Garage provides a fully-fledged REST API to administer your cluster programatically.
|
||||
Functionnality included in the admin API include: setting up and monitoring
|
||||
cluster nodes, managing access credentials, and managing storage buckets and bucket aliases.
|
||||
A full reference of the administration API is available [here](@/documentation/reference-manual/admin-api.md).
|
||||
|
||||
### Metrics and traces
|
||||
|
||||
Garage makes some internal metrics available in the Prometheus data format,
|
||||
which allows you to build interactive dashboards to visualize the load and internal state of your storage cluster.
|
||||
|
||||
For developpers and performance-savvy administrators,
|
||||
Garage also supports exporting traces of what it does internally in OpenTelemetry format.
|
||||
This allows to monitor the time spent at various steps of the processing of requests,
|
||||
in order to detect potential performance bottlenecks.
|
||||
|
||||
### Kubernetes and Nomad integrations
|
||||
|
||||
Garage can automatically discover other nodes in the cluster thanks to integration
|
||||
with orchestrators such as Kubernetes and Nomad (when used with Consul).
|
||||
This eases the configuration of your cluster as it removes one step where nodes need
|
||||
to be manually connected to one another.
|
||||
|
||||
### Support for changing IP addresses
|
||||
|
||||
As long as all of your nodes don't thange their IP address at the same time,
|
||||
Garage should be able to tolerate nodes with changing/dynamic IP addresses,
|
||||
as nodes will regularly exchange the IP addresses of their peers and try to
|
||||
reconnect using newer addresses when existing connections are broken.
|
||||
|
||||
### K2V API (experimental)
|
||||
|
||||
As part of an ongoing research project, Garage can expose an experimental key/value storage API called K2V.
|
||||
K2V is made for the storage and retrieval of many small key/value pairs that need to be processed in bulk.
|
||||
This completes the S3 API with an alternative that can be used to easily store and access metadata
|
||||
related to objects stored in an S3 bucket.
|
||||
|
||||
In the context of our research project, [Aérogramme](https://aerogramme.deuxfleurs.fr),
|
||||
K2V is used to provide metadata and log storage for operations on encrypted e-mail storage.
|
||||
|
||||
Learn more on the specification of K2V [here](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/k2v/doc/drafts/k2v-spec.md)
|
||||
and on how to enable it in Garage [here](@/documentation/reference-manual/k2v.md).
|
||||
@@ -1,6 +1,6 @@
|
||||
+++
|
||||
title = "K2V"
|
||||
weight = 30
|
||||
weight = 70
|
||||
+++
|
||||
|
||||
Starting with version 0.7.2, Garage introduces an optionnal feature, K2V,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
+++
|
||||
title = "Cluster layout management"
|
||||
weight = 10
|
||||
weight = 50
|
||||
+++
|
||||
|
||||
The cluster layout in Garage is a table that assigns to each node a role in
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
+++
|
||||
title = "Request routing logic"
|
||||
weight = 10
|
||||
+++
|
||||
|
||||
Data retrieval requests to Garage endpoints (S3 API and websites) are resolved
|
||||
to an individual object in a bucket. Since objects are replicated to multiple nodes
|
||||
Garage must ensure consistency before answering the request.
|
||||
|
||||
## Using quorum to ensure consistency
|
||||
|
||||
Garage ensures consistency by attempting to establish a quorum with the
|
||||
data nodes responsible for the object. When a majority of the data nodes
|
||||
have provided metadata on a object Garage can then answer the request.
|
||||
|
||||
When a request arrives Garage will, assuming the recommended 3 replicas, perform the following actions:
|
||||
|
||||
- Make a request to the two preferred nodes for object metadata
|
||||
- Try the third node if one of the two initial requests fail
|
||||
- Check that the metadata from at least 2 nodes match
|
||||
- Check that the object hasn't been marked deleted
|
||||
- Answer the request with inline data from metadata if object is small enough
|
||||
- Or get data blocks from the preferred nodes and answer using the assembled object
|
||||
|
||||
Garage dynamically determines which nodes to query based on health, preference, and
|
||||
which nodes actually host a given data. Garage has no concept of "primary" so any
|
||||
healthy node with the data can be used as long as a quorum is reached for the metadata.
|
||||
|
||||
## Node health
|
||||
|
||||
Garage keeps a TCP session open to each node in the cluster and periodically pings them. If a connection
|
||||
cannot be established, or a node fails to answer a number of pings, the target node is marked as failed.
|
||||
Failed nodes are not used for quorum or other internal requests.
|
||||
|
||||
## Node preference
|
||||
|
||||
Garage prioritizes which nodes to query according to a few criteria:
|
||||
|
||||
- A node always prefers itself if it can answer the request
|
||||
- Then the node prioritizes nodes in the same zone
|
||||
- Finally the nodes with the lowest latency are prioritized
|
||||
|
||||
|
||||
For further reading on the cluster structure look at the [gateway](@/documentation/cookbook/gateways.md)
|
||||
and [cluster layout management](@/documentation/reference-manual/layout.md) pages.
|
||||
@@ -1,6 +1,6 @@
|
||||
+++
|
||||
title = "S3 Compatibility status"
|
||||
weight = 20
|
||||
weight = 40
|
||||
+++
|
||||
|
||||
## DISCLAIMER
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
+++
|
||||
title = "Design draft"
|
||||
weight = 25
|
||||
title = "Design draft (obsolete)"
|
||||
weight = 50
|
||||
+++
|
||||
|
||||
**WARNING: this documentation is a design draft which was written before Garage's actual implementation.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
+++
|
||||
title = "Load balancing data"
|
||||
weight = 10
|
||||
title = "Load balancing data (obsolete)"
|
||||
weight = 60
|
||||
+++
|
||||
|
||||
**This is being yet improved in release 0.5. The working document has not been updated yet, it still only applies to Garage 0.2 through 0.4.**
|
||||
|
||||
+5
-7
@@ -4,18 +4,16 @@ rec {
|
||||
*/
|
||||
pkgsSrc = fetchTarball {
|
||||
# As of 2021-10-04
|
||||
url ="https://github.com/NixOS/nixpkgs/archive/b27d18a412b071f5d7991d1648cfe78ee7afe68a.tar.gz";
|
||||
url = "https://github.com/NixOS/nixpkgs/archive/b27d18a412b071f5d7991d1648cfe78ee7afe68a.tar.gz";
|
||||
sha256 = "1xy9zpypqfxs5gcq5dcla4bfkhxmh5nzn9dyqkr03lqycm9wg5cr";
|
||||
};
|
||||
cargo2nixSrc = fetchGit {
|
||||
# As of 2022-03-17
|
||||
url = "https://github.com/superboum/cargo2nix";
|
||||
ref = "main";
|
||||
rev = "bcbf3ba99e9e01a61eb83a24624419c2dd9dec64";
|
||||
# As of 2022-08-29, stacking two patches: superboum@dedup_propagate and Alexis211@fix_fetchcrategit
|
||||
url = "https://github.com/Alexis211/cargo2nix";
|
||||
ref = "fix_fetchcrategit";
|
||||
rev = "4b31c0cc05b6394916d46e9289f51263d81973b9";
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Shared objects
|
||||
*/
|
||||
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
{
|
||||
system ? builtins.currentSystem,
|
||||
target ? null,
|
||||
compiler ? "rustc",
|
||||
release ? false,
|
||||
git_version ? null,
|
||||
}:
|
||||
|
||||
with import ./common.nix;
|
||||
|
||||
let
|
||||
log = v: builtins.trace v v;
|
||||
|
||||
pkgs = import pkgsSrc {
|
||||
inherit system;
|
||||
${ if target == null then null else "crossSystem" } = { config = target; };
|
||||
overlays = [ cargo2nixOverlay ];
|
||||
};
|
||||
|
||||
/*
|
||||
Rust and Nix triples are not the same. Cargo2nix has a dedicated library
|
||||
to convert Nix triples to Rust ones. We need this conversion as we want to
|
||||
set later options linked to our (rust) target in a generic way. Not only
|
||||
the triple terminology is different, but also the "roles" are named differently.
|
||||
Nix uses a build/host/target terminology where Nix's "host" maps to Cargo's "target".
|
||||
*/
|
||||
rustTarget = log (pkgs.rustBuilder.rustLib.rustTriple pkgs.stdenv.hostPlatform);
|
||||
|
||||
/*
|
||||
Cargo2nix is built for rustOverlay which installs Rust from Mozilla releases.
|
||||
We want our own Rust to avoid incompatibilities, like we had with musl 1.2.0.
|
||||
rustc was built with musl < 1.2.0 and nix shipped musl >= 1.2.0 which lead to compilation breakage.
|
||||
So we want a Rust release that is bound to our Nix repository to avoid these problems.
|
||||
See here for more info: https://musl.libc.org/time64.html
|
||||
Because Cargo2nix does not support the Rust environment shipped by NixOS,
|
||||
we emulate the structure of the Rust object created by rustOverlay.
|
||||
In practise, rustOverlay ships rustc+cargo in a single derivation while
|
||||
NixOS ships them in separate ones. We reunite them with symlinkJoin.
|
||||
*/
|
||||
rustChannel = {
|
||||
rustc = pkgs.symlinkJoin {
|
||||
name = "rust-channel";
|
||||
paths = [
|
||||
pkgs.rustPlatform.rust.cargo
|
||||
pkgs.rustPlatform.rust.rustc
|
||||
];
|
||||
};
|
||||
clippy = pkgs.symlinkJoin {
|
||||
name = "clippy-channel";
|
||||
paths = [
|
||||
pkgs.rustPlatform.rust.cargo
|
||||
pkgs.rustPlatform.rust.rustc
|
||||
pkgs.clippy
|
||||
];
|
||||
};
|
||||
}.${compiler};
|
||||
|
||||
clippyBuilder = pkgs.writeScriptBin "clippy" ''
|
||||
#!${pkgs.stdenv.shell}
|
||||
. ${cargo2nixSrc + "/overlay/utils.sh"}
|
||||
isBuildScript=
|
||||
args=("$@")
|
||||
for i in "''${!args[@]}"; do
|
||||
if [ "xmetadata=" = "x''${args[$i]::9}" ]; then
|
||||
args[$i]=metadata=$NIX_RUST_METADATA
|
||||
elif [ "x--crate-name" = "x''${args[$i]}" ] && [ "xbuild_script_" = "x''${args[$i+1]::13}" ]; then
|
||||
isBuildScript=1
|
||||
fi
|
||||
done
|
||||
if [ "$isBuildScript" ]; then
|
||||
args+=($NIX_RUST_BUILD_LINK_FLAGS)
|
||||
else
|
||||
args+=($NIX_RUST_LINK_FLAGS)
|
||||
fi
|
||||
touch invoke.log
|
||||
echo "''${args[@]}" >>invoke.log
|
||||
|
||||
exec ${rustChannel}/bin/clippy-driver --deny warnings "''${args[@]}"
|
||||
'';
|
||||
|
||||
buildEnv = (drv: {
|
||||
rustc = drv.setBuildEnv;
|
||||
clippy = ''
|
||||
${drv.setBuildEnv or "" }
|
||||
echo
|
||||
echo --- BUILDING WITH CLIPPY ---
|
||||
echo
|
||||
|
||||
export RUSTC=${clippyBuilder}/bin/clippy
|
||||
'';
|
||||
}.${compiler});
|
||||
|
||||
/*
|
||||
Cargo2nix provides many overrides by default, you can take inspiration from them:
|
||||
https://github.com/cargo2nix/cargo2nix/blob/master/overlay/overrides.nix
|
||||
|
||||
You can have a complete list of the available options by looking at the overriden object, mkcrate:
|
||||
https://github.com/cargo2nix/cargo2nix/blob/master/overlay/mkcrate.nix
|
||||
*/
|
||||
overrides = pkgs.rustBuilder.overrides.all ++ [
|
||||
/*
|
||||
[1] We add some logic to compile our crates with clippy, it provides us many additional lints
|
||||
|
||||
[2] We need to alter Nix hardening to make static binaries: PIE,
|
||||
Position Independent Executables seems to be supported only on amd64. Having
|
||||
this flag set either 1. make our executables crash or 2. compile as dynamic on some platforms.
|
||||
Here, we deactivate it. Later (find `codegenOpts`), we reactivate it for supported targets
|
||||
(only amd64 curently) through the `-static-pie` flag.
|
||||
PIE is a feature used by ASLR, which helps mitigate security issues.
|
||||
Learn more about Nix Hardening at: https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/cc-wrapper/add-hardening.sh
|
||||
|
||||
[3] We want to inject the git version while keeping the build deterministic.
|
||||
As we do not want to consider the .git folder as part of the input source,
|
||||
we ask the user (the CI often) to pass the value to Nix.
|
||||
|
||||
[4] We ship some parts of the code disabled by default by putting them behind a flag.
|
||||
It speeds up the compilation (when the feature is not required) and released crates have less dependency by default (less attack surface, disk space, etc.).
|
||||
But we want to ship these additional features when we release Garage.
|
||||
In the end, we chose to exclude all features from debug builds while putting (all of) them in the release builds.
|
||||
|
||||
[5] We don't want libsodium-sys and zstd-sys to try to use pkgconfig to build against a system library.
|
||||
However the features to do so get activated for some reason (due to a bug in cargo2nix?),
|
||||
so disable them manually here.
|
||||
*/
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "garage";
|
||||
overrideAttrs = drv:
|
||||
(if git_version != null then {
|
||||
/* [3] */ preConfigure = ''
|
||||
${drv.preConfigure or ""}
|
||||
export GIT_VERSION="${git_version}"
|
||||
'';
|
||||
} else {})
|
||||
//
|
||||
{
|
||||
/* [1] */ setBuildEnv = (buildEnv drv);
|
||||
/* [2] */ hardeningDisable = [ "pie" ];
|
||||
};
|
||||
overrideArgs = old: {
|
||||
/* [4] */ features = [ "bundled-libs" "sled" ]
|
||||
++ (if release then [ "kubernetes-discovery" "telemetry-otlp" "metrics" "lmdb" "sqlite" ] else []);
|
||||
};
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "garage_rpc";
|
||||
overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); };
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "garage_db";
|
||||
overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); };
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "garage_util";
|
||||
overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); };
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "garage_table";
|
||||
overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); };
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "garage_block";
|
||||
overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); };
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "garage_model";
|
||||
overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); };
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "garage_api";
|
||||
overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); };
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "garage_web";
|
||||
overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); };
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "k2v-client";
|
||||
overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); };
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "libsodium-sys";
|
||||
overrideArgs = old: {
|
||||
features = [ ]; /* [5] */
|
||||
};
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "zstd-sys";
|
||||
overrideArgs = old: {
|
||||
features = [ ]; /* [5] */
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
packageFun = import ../Cargo.nix;
|
||||
|
||||
/*
|
||||
We compile fully static binaries with musl to simplify deployment on most systems.
|
||||
When possible, we reactivate PIE hardening (see above).
|
||||
|
||||
Also, if you set the RUSTFLAGS environment variable, the following parameters will
|
||||
be ignored.
|
||||
|
||||
For more information on static builds, please refer to Rust's RFC 1721.
|
||||
https://rust-lang.github.io/rfcs/1721-crt-static.html#specifying-dynamicstatic-c-runtime-linkage
|
||||
*/
|
||||
|
||||
codegenOpts = {
|
||||
"armv6l-unknown-linux-musleabihf" = [ "target-feature=+crt-static" "link-arg=-static" ]; /* compile as dynamic with static-pie */
|
||||
"aarch64-unknown-linux-musl" = [ "target-feature=+crt-static" "link-arg=-static" ]; /* segfault with static-pie */
|
||||
"i686-unknown-linux-musl" = [ "target-feature=+crt-static" "link-arg=-static" ]; /* segfault with static-pie */
|
||||
"x86_64-unknown-linux-musl" = [ "target-feature=+crt-static" "link-arg=-static-pie" ];
|
||||
};
|
||||
|
||||
in
|
||||
/*
|
||||
The following definition is not elegant as we use a low level function of Cargo2nix
|
||||
that enables us to pass our custom rustChannel object. We need this low level definition
|
||||
to pass Nix's Rust toolchains instead of Mozilla's one.
|
||||
|
||||
target is mandatory but must be kept to null to allow cargo2nix to set it to the appropriate value
|
||||
for each crate.
|
||||
*/
|
||||
pkgs.rustBuilder.makePackageSet {
|
||||
inherit packageFun rustChannel release codegenOpts;
|
||||
packageOverrides = overrides;
|
||||
target = null;
|
||||
|
||||
buildRustPackages = pkgs.buildPackages.rustBuilder.makePackageSet {
|
||||
inherit rustChannel packageFun codegenOpts;
|
||||
packageOverrides = overrides;
|
||||
target = null;
|
||||
};
|
||||
}
|
||||
+6
-1
@@ -1,4 +1,9 @@
|
||||
substituters = https://cache.nixos.org https://nix.web.deuxfleurs.fr
|
||||
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= nix.web.deuxfleurs.fr:eTGL6kvaQn6cDR/F9lDYUIP9nCVR/kkshYfLDJf1yKs=
|
||||
max-jobs = auto
|
||||
cores = 4
|
||||
cores = 0
|
||||
log-lines = 200
|
||||
filter-syscalls = false
|
||||
sandbox = false
|
||||
keep-outputs = true
|
||||
keep-derivations = true
|
||||
|
||||
@@ -11,7 +11,7 @@ PATH="${GARAGE_DEBUG}:${GARAGE_RELEASE}:${NIX_RELEASE}:$PATH"
|
||||
FANCYCOLORS=("41m" "42m" "44m" "45m" "100m" "104m")
|
||||
|
||||
export RUST_BACKTRACE=1
|
||||
export RUST_LOG=garage=info,garage_api=debug
|
||||
export RUST_LOG=garage=info,garage_api=debug,netapp=trace
|
||||
MAIN_LABEL="\e[${FANCYCOLORS[0]}[main]\e[49m"
|
||||
|
||||
WHICH_GARAGE=$(which garage || exit 1)
|
||||
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "[fail] usage: $0 binary"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if file $1 | grep 'dynamically linked' 2>&1; then
|
||||
echo "[fail] $1 is dynamic"
|
||||
exit 1
|
||||
fi
|
||||
echo "[ok] $1 is probably static"
|
||||
@@ -1,8 +1,5 @@
|
||||
{
|
||||
system ? builtins.currentSystem,
|
||||
rust ? true,
|
||||
integration ? true,
|
||||
release ? true,
|
||||
}:
|
||||
|
||||
with import ./nix/common.nix;
|
||||
@@ -16,9 +13,59 @@ let
|
||||
winscp = (import ./nix/winscp.nix) pkgs;
|
||||
|
||||
in
|
||||
{
|
||||
|
||||
pkgs.mkShell {
|
||||
shellHook = ''
|
||||
/* --- Rust Shell ---
|
||||
* Use it to compile Garage
|
||||
*/
|
||||
rust = pkgs.mkShell {
|
||||
shellHook = ''
|
||||
function refresh_toolchain {
|
||||
nix copy \
|
||||
--to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/etc/nix/signing-key.sec' \
|
||||
$(nix-store -qR \
|
||||
$(nix-build --quiet --no-build-output --no-out-link nix/toolchain.nix))
|
||||
}
|
||||
'';
|
||||
|
||||
nativeBuildInputs = [
|
||||
#pkgs.rustPlatform.rust.rustc
|
||||
pkgs.rustPlatform.rust.cargo
|
||||
#pkgs.clippy
|
||||
pkgs.rustfmt
|
||||
#pkgs.perl
|
||||
#pkgs.protobuf
|
||||
#pkgs.pkg-config
|
||||
#pkgs.openssl
|
||||
pkgs.file
|
||||
#cargo2nix.packages.x86_64-linux.cargo2nix
|
||||
];
|
||||
};
|
||||
|
||||
/* --- Integration shell ---
|
||||
* Use it to test Garage with common S3 clients
|
||||
*/
|
||||
integration = pkgs.mkShell {
|
||||
nativeBuildInputs = [
|
||||
winscp
|
||||
pkgs.s3cmd
|
||||
pkgs.awscli2
|
||||
pkgs.minio-client
|
||||
pkgs.rclone
|
||||
pkgs.socat
|
||||
pkgs.psmisc
|
||||
pkgs.which
|
||||
pkgs.openssl
|
||||
pkgs.curl
|
||||
pkgs.jq
|
||||
];
|
||||
};
|
||||
|
||||
/* --- Release shell ---
|
||||
* A shell built to make releasing easier
|
||||
*/
|
||||
release = pkgs.mkShell {
|
||||
shellHook = ''
|
||||
function to_s3 {
|
||||
aws \
|
||||
--endpoint-url https://garage.deuxfleurs.fr \
|
||||
@@ -62,45 +109,12 @@ function refresh_index {
|
||||
result/share/_releases.html \
|
||||
s3://garagehq.deuxfleurs.fr/
|
||||
}
|
||||
'';
|
||||
nativeBuildInputs = [
|
||||
pkgs.awscli2
|
||||
kaniko
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
function refresh_toolchain {
|
||||
nix copy \
|
||||
--to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/etc/nix/signing-key.sec' \
|
||||
$(nix-store -qR \
|
||||
$(nix-build --quiet --no-build-output --no-out-link nix/toolchain.nix))
|
||||
}
|
||||
'';
|
||||
|
||||
nativeBuildInputs =
|
||||
(if rust then [
|
||||
pkgs.rustPlatform.rust.rustc
|
||||
pkgs.rustPlatform.rust.cargo
|
||||
pkgs.clippy
|
||||
pkgs.rustfmt
|
||||
pkgs.perl
|
||||
pkgs.protobuf
|
||||
pkgs.pkg-config
|
||||
pkgs.openssl
|
||||
cargo2nix.packages.x86_64-linux.cargo2nix
|
||||
] else [])
|
||||
++
|
||||
(if integration then [
|
||||
winscp
|
||||
pkgs.s3cmd
|
||||
pkgs.awscli2
|
||||
pkgs.minio-client
|
||||
pkgs.rclone
|
||||
pkgs.socat
|
||||
pkgs.psmisc
|
||||
pkgs.which
|
||||
pkgs.openssl
|
||||
pkgs.curl
|
||||
pkgs.jq
|
||||
] else [])
|
||||
++
|
||||
(if release then [
|
||||
pkgs.awscli2
|
||||
kaniko
|
||||
] else [])
|
||||
;
|
||||
}
|
||||
|
||||
+14
-13
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_api"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
@@ -14,30 +14,31 @@ path = "lib.rs"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
garage_model = { version = "0.7.0", path = "../model" }
|
||||
garage_table = { version = "0.7.0", path = "../table" }
|
||||
garage_block = { version = "0.7.0", path = "../block" }
|
||||
garage_util = { version = "0.7.0", path = "../util" }
|
||||
garage_rpc = { version = "0.7.0", path = "../rpc" }
|
||||
garage_model = { version = "0.8.0", path = "../model" }
|
||||
garage_table = { version = "0.8.0", path = "../table" }
|
||||
garage_block = { version = "0.8.0", path = "../block" }
|
||||
garage_util = { version = "0.8.0", path = "../util" }
|
||||
garage_rpc = { version = "0.8.0", path = "../rpc" }
|
||||
|
||||
async-trait = "0.1.7"
|
||||
base64 = "0.13"
|
||||
bytes = "1.0"
|
||||
chrono = "0.4"
|
||||
crypto-mac = "0.10"
|
||||
crypto-common = "0.1"
|
||||
err-derive = "0.3"
|
||||
hex = "0.4"
|
||||
hmac = "0.10"
|
||||
hmac = "0.12"
|
||||
idna = "0.2"
|
||||
tracing = "0.1.30"
|
||||
md-5 = "0.9"
|
||||
md-5 = "0.10"
|
||||
nom = "7.1"
|
||||
sha2 = "0.9"
|
||||
sha2 = "0.10"
|
||||
|
||||
futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
pin-project = "1.0"
|
||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
||||
tokio-stream = "0.1"
|
||||
|
||||
form_urlencoded = "1.0.0"
|
||||
http = "0.2"
|
||||
@@ -54,9 +55,9 @@ quick-xml = { version = "0.21", features = [ "serialize" ] }
|
||||
url = "2.1"
|
||||
|
||||
opentelemetry = "0.17"
|
||||
opentelemetry-prometheus = "0.10"
|
||||
opentelemetry-otlp = "0.10"
|
||||
prometheus = "0.13"
|
||||
opentelemetry-prometheus = { version = "0.10", optional = true }
|
||||
prometheus = { version = "0.13", optional = true }
|
||||
|
||||
[features]
|
||||
k2v = [ "garage_util/k2v", "garage_model/k2v" ]
|
||||
metrics = [ "opentelemetry-prometheus", "prometheus" ]
|
||||
|
||||
+40
-28
@@ -1,15 +1,17 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use futures::future::Future;
|
||||
use http::header::{
|
||||
ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW, CONTENT_TYPE,
|
||||
};
|
||||
use http::header::{ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW};
|
||||
use hyper::{Body, Request, Response};
|
||||
|
||||
use opentelemetry::trace::{SpanRef, Tracer};
|
||||
use opentelemetry::trace::SpanRef;
|
||||
|
||||
#[cfg(feature = "metrics")]
|
||||
use opentelemetry_prometheus::PrometheusExporter;
|
||||
#[cfg(feature = "metrics")]
|
||||
use prometheus::{Encoder, TextEncoder};
|
||||
|
||||
use garage_model::garage::Garage;
|
||||
@@ -25,6 +27,7 @@ use crate::admin::router::{Authorization, Endpoint};
|
||||
|
||||
pub struct AdminApiServer {
|
||||
garage: Arc<Garage>,
|
||||
#[cfg(feature = "metrics")]
|
||||
exporter: PrometheusExporter,
|
||||
metrics_token: Option<String>,
|
||||
admin_token: Option<String>,
|
||||
@@ -32,7 +35,6 @@ pub struct AdminApiServer {
|
||||
|
||||
impl AdminApiServer {
|
||||
pub fn new(garage: Arc<Garage>) -> Self {
|
||||
let exporter = opentelemetry_prometheus::exporter().init();
|
||||
let cfg = &garage.config.admin;
|
||||
let metrics_token = cfg
|
||||
.metrics_token
|
||||
@@ -44,21 +46,22 @@ impl AdminApiServer {
|
||||
.map(|tok| format!("Bearer {}", tok));
|
||||
Self {
|
||||
garage,
|
||||
exporter,
|
||||
#[cfg(feature = "metrics")]
|
||||
exporter: opentelemetry_prometheus::exporter().init(),
|
||||
metrics_token,
|
||||
admin_token,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(self, shutdown_signal: impl Future<Output = ()>) -> Result<(), GarageError> {
|
||||
if let Some(bind_addr) = self.garage.config.admin.api_bind_addr {
|
||||
let region = self.garage.config.s3_api.s3_region.clone();
|
||||
ApiServer::new(region, self)
|
||||
.run_server(bind_addr, shutdown_signal)
|
||||
.await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
pub async fn run(
|
||||
self,
|
||||
bind_addr: SocketAddr,
|
||||
shutdown_signal: impl Future<Output = ()>,
|
||||
) -> Result<(), GarageError> {
|
||||
let region = self.garage.config.s3_api.s3_region.clone();
|
||||
ApiServer::new(region, self)
|
||||
.run_server(bind_addr, shutdown_signal)
|
||||
.await
|
||||
}
|
||||
|
||||
fn handle_options(&self, _req: &Request<Body>) -> Result<Response<Body>, Error> {
|
||||
@@ -71,22 +74,31 @@ impl AdminApiServer {
|
||||
}
|
||||
|
||||
fn handle_metrics(&self) -> Result<Response<Body>, Error> {
|
||||
let mut buffer = vec![];
|
||||
let encoder = TextEncoder::new();
|
||||
#[cfg(feature = "metrics")]
|
||||
{
|
||||
use opentelemetry::trace::Tracer;
|
||||
|
||||
let tracer = opentelemetry::global::tracer("garage");
|
||||
let metric_families = tracer.in_span("admin/gather_metrics", |_| {
|
||||
self.exporter.registry().gather()
|
||||
});
|
||||
let mut buffer = vec![];
|
||||
let encoder = TextEncoder::new();
|
||||
|
||||
encoder
|
||||
.encode(&metric_families, &mut buffer)
|
||||
.ok_or_internal_error("Could not serialize metrics")?;
|
||||
let tracer = opentelemetry::global::tracer("garage");
|
||||
let metric_families = tracer.in_span("admin/gather_metrics", |_| {
|
||||
self.exporter.registry().gather()
|
||||
});
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(200)
|
||||
.header(CONTENT_TYPE, encoder.format_type())
|
||||
.body(Body::from(buffer))?)
|
||||
encoder
|
||||
.encode(&metric_families, &mut buffer)
|
||||
.ok_or_internal_error("Could not serialize metrics")?;
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(200)
|
||||
.header(http::header::CONTENT_TYPE, encoder.format_type())
|
||||
.body(Body::from(buffer))?)
|
||||
}
|
||||
#[cfg(not(feature = "metrics"))]
|
||||
Err(Error::bad_request(
|
||||
"Garage was built without the metrics feature".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ use crate::helpers::{json_ok_response, parse_json_body};
|
||||
pub async fn handle_get_cluster_status(garage: &Arc<Garage>) -> Result<Response<Body>, Error> {
|
||||
let res = GetClusterStatusResponse {
|
||||
node: hex::encode(garage.system.id),
|
||||
garage_version: garage.system.garage_version(),
|
||||
garage_version: garage_util::version::garage_version(),
|
||||
garage_features: garage_util::version::garage_features(),
|
||||
db_engine: garage.db.engine(),
|
||||
known_nodes: garage
|
||||
.system
|
||||
@@ -99,6 +100,7 @@ fn get_cluster_layout(garage: &Arc<Garage>) -> GetClusterLayoutResponse {
|
||||
struct GetClusterStatusResponse {
|
||||
node: String,
|
||||
garage_version: &'static str,
|
||||
garage_features: Option<&'static [&'static str]>,
|
||||
db_engine: String,
|
||||
known_nodes: HashMap<String, KnownNodeResp>,
|
||||
layout: GetClusterLayoutResponse,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
@@ -36,20 +37,13 @@ pub(crate) struct K2VApiEndpoint {
|
||||
impl K2VApiServer {
|
||||
pub async fn run(
|
||||
garage: Arc<Garage>,
|
||||
bind_addr: SocketAddr,
|
||||
s3_region: String,
|
||||
shutdown_signal: impl Future<Output = ()>,
|
||||
) -> Result<(), GarageError> {
|
||||
if let Some(cfg) = &garage.config.k2v_api {
|
||||
let bind_addr = cfg.api_bind_addr;
|
||||
|
||||
ApiServer::new(
|
||||
garage.config.s3_api.s3_region.clone(),
|
||||
K2VApiServer { garage },
|
||||
)
|
||||
ApiServer::new(s3_region, K2VApiServer { garage })
|
||||
.run_server(bind_addr, shutdown_signal)
|
||||
.await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
@@ -43,16 +44,13 @@ pub(crate) struct S3ApiEndpoint {
|
||||
impl S3ApiServer {
|
||||
pub async fn run(
|
||||
garage: Arc<Garage>,
|
||||
addr: SocketAddr,
|
||||
s3_region: String,
|
||||
shutdown_signal: impl Future<Output = ()>,
|
||||
) -> Result<(), GarageError> {
|
||||
let addr = garage.config.s3_api.api_bind_addr;
|
||||
|
||||
ApiServer::new(
|
||||
garage.config.s3_api.s3_region.clone(),
|
||||
S3ApiServer { garage },
|
||||
)
|
||||
.run_server(addr, shutdown_signal)
|
||||
.await
|
||||
ApiServer::new(s3_region, S3ApiServer { garage })
|
||||
.run_server(addr, shutdown_signal)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn handle_request_without_bucket(
|
||||
|
||||
@@ -295,7 +295,6 @@ fn parse_create_bucket_xml(xml_bytes: &[u8]) -> Option<Option<String>> {
|
||||
|
||||
let mut ret = None;
|
||||
for item in cbc.children() {
|
||||
println!("{:?}", item);
|
||||
if item.has_tag_name("LocationConstraint") {
|
||||
if ret != None {
|
||||
return None;
|
||||
|
||||
+18
-11
@@ -5,9 +5,12 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use futures::{stream, stream::Stream, StreamExt, TryFutureExt};
|
||||
use md5::{Digest as Md5Digest, Md5};
|
||||
|
||||
use bytes::Bytes;
|
||||
use hyper::{Body, Request, Response};
|
||||
use serde::Serialize;
|
||||
|
||||
use garage_rpc::netapp::bytes_buf::BytesBuf;
|
||||
use garage_rpc::rpc_helper::OrderTag;
|
||||
use garage_table::*;
|
||||
use garage_util::data::*;
|
||||
use garage_util::time::*;
|
||||
@@ -305,13 +308,18 @@ pub async fn handle_upload_part_copy(
|
||||
// if and only if the block returned is a block that already existed
|
||||
// in the Garage data store (thus we don't need to save it again).
|
||||
let garage2 = garage.clone();
|
||||
let order_stream = OrderTag::stream();
|
||||
let source_blocks = stream::iter(blocks_to_copy)
|
||||
.flat_map(|(block_hash, range_to_copy)| {
|
||||
.enumerate()
|
||||
.flat_map(|(i, (block_hash, range_to_copy))| {
|
||||
let garage3 = garage2.clone();
|
||||
stream::once(async move {
|
||||
let data = garage3.block_manager.rpc_get_block(&block_hash).await?;
|
||||
let data = garage3
|
||||
.block_manager
|
||||
.rpc_get_block(&block_hash, Some(order_stream.order(i as u64)))
|
||||
.await?;
|
||||
match range_to_copy {
|
||||
Some(r) => Ok((data[r].to_vec(), None)),
|
||||
Some(r) => Ok((data.slice(r), None)),
|
||||
None => Ok((data, Some(block_hash))),
|
||||
}
|
||||
})
|
||||
@@ -553,13 +561,13 @@ impl CopyPreconditionHeaders {
|
||||
}
|
||||
}
|
||||
|
||||
type BlockStreamItemOk = (Vec<u8>, Option<Hash>);
|
||||
type BlockStreamItemOk = (Bytes, Option<Hash>);
|
||||
type BlockStreamItem = Result<BlockStreamItemOk, garage_util::error::Error>;
|
||||
|
||||
struct Defragmenter<S: Stream<Item = BlockStreamItem>> {
|
||||
block_size: usize,
|
||||
block_stream: Pin<Box<stream::Peekable<S>>>,
|
||||
buffer: Vec<u8>,
|
||||
buffer: BytesBuf,
|
||||
hash: Option<Hash>,
|
||||
}
|
||||
|
||||
@@ -568,7 +576,7 @@ impl<S: Stream<Item = BlockStreamItem>> Defragmenter<S> {
|
||||
Self {
|
||||
block_size,
|
||||
block_stream,
|
||||
buffer: vec![],
|
||||
buffer: BytesBuf::new(),
|
||||
hash: None,
|
||||
}
|
||||
}
|
||||
@@ -586,7 +594,7 @@ impl<S: Stream<Item = BlockStreamItem>> Defragmenter<S> {
|
||||
|
||||
if self.buffer.is_empty() {
|
||||
let (next_block, next_block_hash) = self.block_stream.next().await.unwrap()?;
|
||||
self.buffer = next_block;
|
||||
self.buffer.extend(next_block);
|
||||
self.hash = next_block_hash;
|
||||
} else if self.buffer.len() + peeked_next_block.len() > self.block_size {
|
||||
break;
|
||||
@@ -597,11 +605,11 @@ impl<S: Stream<Item = BlockStreamItem>> Defragmenter<S> {
|
||||
}
|
||||
}
|
||||
|
||||
Ok((std::mem::take(&mut self.buffer), self.hash.take()))
|
||||
Ok((self.buffer.take_all(), self.hash.take()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct CopyObjectResult {
|
||||
#[serde(rename = "LastModified")]
|
||||
pub last_modified: s3_xml::Value,
|
||||
@@ -609,7 +617,7 @@ pub struct CopyObjectResult {
|
||||
pub etag: s3_xml::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct CopyPartResult {
|
||||
#[serde(serialize_with = "xmlns_tag")]
|
||||
pub xmlns: (),
|
||||
@@ -654,7 +662,6 @@ mod tests {
|
||||
last_modified: s3_xml::Value("2011-04-11T20:34:56.000Z".into()),
|
||||
etag: s3_xml::Value("\"9b2cf535f27731c974343645a3985328\"".into()),
|
||||
};
|
||||
println!("{}", to_xml_with_header(&v)?);
|
||||
|
||||
assert_eq!(to_xml_with_header(&v)?, expected_retval);
|
||||
|
||||
|
||||
@@ -64,14 +64,13 @@ pub async fn handle_delete(
|
||||
bucket_id: Uuid,
|
||||
key: &str,
|
||||
) -> Result<Response<Body>, Error> {
|
||||
let (_deleted_version, delete_marker_version) =
|
||||
handle_delete_internal(&garage, bucket_id, key).await?;
|
||||
|
||||
Ok(Response::builder()
|
||||
.header("x-amz-version-id", hex::encode(delete_marker_version))
|
||||
.status(StatusCode::NO_CONTENT)
|
||||
.body(Body::from(vec![]))
|
||||
.unwrap())
|
||||
match handle_delete_internal(&garage, bucket_id, key).await {
|
||||
Ok(_) | Err(Error::NoSuchKey) => Ok(Response::builder()
|
||||
.status(StatusCode::NO_CONTENT)
|
||||
.body(Body::from(vec![]))
|
||||
.unwrap()),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_delete_objects(
|
||||
|
||||
+112
-50
@@ -2,16 +2,19 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, UNIX_EPOCH};
|
||||
|
||||
use futures::stream::*;
|
||||
use futures::future;
|
||||
use futures::stream::{self, StreamExt};
|
||||
use http::header::{
|
||||
ACCEPT_RANGES, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, ETAG, IF_MODIFIED_SINCE,
|
||||
IF_NONE_MATCH, LAST_MODIFIED, RANGE,
|
||||
};
|
||||
use hyper::body::Bytes;
|
||||
use hyper::{Body, Request, Response, StatusCode};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use garage_rpc::rpc_helper::{netapp::stream::ByteStream, OrderTag};
|
||||
use garage_table::EmptyKey;
|
||||
use garage_util::data::*;
|
||||
use garage_util::error::OkOrMessage;
|
||||
|
||||
use garage_model::garage::Garage;
|
||||
use garage_model::s3::object_table::*;
|
||||
@@ -242,36 +245,56 @@ 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 get_next_blocks = garage.version_table.get(&last_v.uuid, &EmptyKey);
|
||||
let (tx, rx) = mpsc::channel(2);
|
||||
|
||||
let (first_block, version) = futures::try_join!(read_first_block, get_next_blocks)?;
|
||||
let version = version.ok_or(Error::NoSuchKey)?;
|
||||
let order_stream = OrderTag::stream();
|
||||
let first_block_hash = *first_block_hash;
|
||||
let version_uuid = last_v.uuid;
|
||||
|
||||
let mut blocks = version
|
||||
.blocks
|
||||
.items()
|
||||
.iter()
|
||||
.map(|(_, vb)| (vb.hash, None))
|
||||
.collect::<Vec<_>>();
|
||||
blocks[0].1 = Some(first_block);
|
||||
tokio::spawn(async move {
|
||||
match async {
|
||||
let garage2 = garage.clone();
|
||||
let version_fut = tokio::spawn(async move {
|
||||
garage2.version_table.get(&version_uuid, &EmptyKey).await
|
||||
});
|
||||
|
||||
let body_stream = futures::stream::iter(blocks)
|
||||
.map(move |(hash, data_opt)| {
|
||||
let garage = garage.clone();
|
||||
async move {
|
||||
if let Some(data) = data_opt {
|
||||
Ok(Bytes::from(data))
|
||||
} else {
|
||||
garage
|
||||
.block_manager
|
||||
.rpc_get_block(&hash)
|
||||
.await
|
||||
.map(Bytes::from)
|
||||
}
|
||||
let stream_block_0 = garage
|
||||
.block_manager
|
||||
.rpc_get_block_streaming(&first_block_hash, Some(order_stream.order(0)))
|
||||
.await?;
|
||||
tx.send(stream_block_0)
|
||||
.await
|
||||
.ok_or_message("channel closed")?;
|
||||
|
||||
let version = version_fut.await.unwrap()?.ok_or(Error::NoSuchKey)?;
|
||||
for (i, (_, vb)) in version.blocks.items().iter().enumerate().skip(1) {
|
||||
let stream_block_i = garage
|
||||
.block_manager
|
||||
.rpc_get_block_streaming(&vb.hash, Some(order_stream.order(i as u64)))
|
||||
.await?;
|
||||
tx.send(stream_block_i)
|
||||
.await
|
||||
.ok_or_message("channel closed")?;
|
||||
}
|
||||
})
|
||||
.buffered(2);
|
||||
|
||||
Ok::<(), Error>(())
|
||||
}
|
||||
.await
|
||||
{
|
||||
Ok(()) => (),
|
||||
Err(e) => {
|
||||
let err = std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Error while getting object data: {}", e),
|
||||
);
|
||||
let _ = tx
|
||||
.send(Box::pin(stream::once(future::ready(Err(err)))))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let body_stream = tokio_stream::wrappers::ReceiverStream::new(rx).flatten();
|
||||
|
||||
let body = hyper::body::Body::wrap_stream(body_stream);
|
||||
Ok(resp_builder.body(body)?)
|
||||
@@ -422,40 +445,79 @@ fn body_from_blocks_range(
|
||||
all_blocks.len(),
|
||||
4 + ((end - begin) / std::cmp::max(all_blocks[0].1.size as u64, 1024)) as usize,
|
||||
));
|
||||
let mut true_offset = 0;
|
||||
let mut block_offset: u64 = 0;
|
||||
for (_, b) in all_blocks.iter() {
|
||||
if true_offset >= end {
|
||||
if block_offset >= end {
|
||||
break;
|
||||
}
|
||||
// Keep only blocks that have an intersection with the requested range
|
||||
if true_offset < end && true_offset + b.size > begin {
|
||||
blocks.push((*b, true_offset));
|
||||
if block_offset < end && block_offset + b.size > begin {
|
||||
blocks.push((*b, block_offset));
|
||||
}
|
||||
true_offset += b.size;
|
||||
block_offset += b.size as u64;
|
||||
}
|
||||
|
||||
let order_stream = OrderTag::stream();
|
||||
let body_stream = futures::stream::iter(blocks)
|
||||
.map(move |(block, true_offset)| {
|
||||
.enumerate()
|
||||
.map(move |(i, (block, block_offset))| {
|
||||
let garage = garage.clone();
|
||||
async move {
|
||||
let data = garage.block_manager.rpc_get_block(&block.hash).await?;
|
||||
let data = Bytes::from(data);
|
||||
let start_in_block = if true_offset > begin {
|
||||
0
|
||||
} else {
|
||||
begin - true_offset
|
||||
};
|
||||
let end_in_block = if true_offset + block.size < end {
|
||||
block.size
|
||||
} else {
|
||||
end - true_offset
|
||||
};
|
||||
Result::<Bytes, Error>::Ok(
|
||||
data.slice(start_in_block as usize..end_in_block as usize),
|
||||
)
|
||||
garage
|
||||
.block_manager
|
||||
.rpc_get_block_streaming(&block.hash, Some(order_stream.order(i as u64)))
|
||||
.await
|
||||
.unwrap_or_else(|e| error_stream(i, e))
|
||||
.scan(block_offset, move |chunk_offset, chunk| {
|
||||
let r = match chunk {
|
||||
Ok(chunk_bytes) => {
|
||||
let chunk_len = chunk_bytes.len() as u64;
|
||||
let r = if *chunk_offset >= end {
|
||||
// The current chunk is after the part we want to read.
|
||||
// Returning None here will stop the scan, the rest of the
|
||||
// stream will be ignored
|
||||
None
|
||||
} else if *chunk_offset + chunk_len <= begin {
|
||||
// The current chunk is before the part we want to read.
|
||||
// We return a None that will be removed by the filter_map
|
||||
// below.
|
||||
Some(None)
|
||||
} else {
|
||||
// The chunk has an intersection with the requested range
|
||||
let start_in_chunk = if *chunk_offset > begin {
|
||||
0
|
||||
} else {
|
||||
begin - *chunk_offset
|
||||
};
|
||||
let end_in_chunk = if *chunk_offset + chunk_len < end {
|
||||
chunk_len
|
||||
} else {
|
||||
end - *chunk_offset
|
||||
};
|
||||
Some(Some(Ok(chunk_bytes
|
||||
.slice(start_in_chunk as usize..end_in_chunk as usize))))
|
||||
};
|
||||
*chunk_offset += chunk_bytes.len() as u64;
|
||||
r
|
||||
}
|
||||
Err(e) => Some(Some(Err(e))),
|
||||
};
|
||||
futures::future::ready(r)
|
||||
})
|
||||
.filter_map(futures::future::ready)
|
||||
}
|
||||
})
|
||||
.buffered(2);
|
||||
.buffered(2)
|
||||
.flatten();
|
||||
|
||||
hyper::body::Body::wrap_stream(body_stream)
|
||||
}
|
||||
|
||||
fn error_stream(i: usize, e: garage_util::error::Error) -> ByteStream {
|
||||
Box::pin(futures::stream::once(async move {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Could not get block {}: {}", i, e),
|
||||
))
|
||||
}))
|
||||
}
|
||||
|
||||
+42
-22
@@ -1,4 +1,4 @@
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::prelude::*;
|
||||
@@ -8,7 +8,14 @@ use hyper::{Request, Response};
|
||||
use md5::{digest::generic_array::*, Digest as Md5Digest, Md5};
|
||||
use sha2::Sha256;
|
||||
|
||||
use opentelemetry::{
|
||||
trace::{FutureExt as OtelFutureExt, TraceContextExt, Tracer},
|
||||
Context,
|
||||
};
|
||||
|
||||
use garage_rpc::netapp::bytes_buf::BytesBuf;
|
||||
use garage_table::*;
|
||||
use garage_util::async_hash::*;
|
||||
use garage_util::data::*;
|
||||
use garage_util::error::Error as GarageError;
|
||||
use garage_util::time::*;
|
||||
@@ -102,7 +109,7 @@ pub(crate) async fn save_stream<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
|
||||
size,
|
||||
etag: data_md5sum_hex.clone(),
|
||||
},
|
||||
first_block,
|
||||
first_block.to_vec(),
|
||||
)),
|
||||
};
|
||||
|
||||
@@ -130,7 +137,7 @@ pub(crate) async fn save_stream<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
|
||||
garage.version_table.insert(&version).await?;
|
||||
|
||||
// Transfer data and verify checksum
|
||||
let first_block_hash = blake2sum(&first_block[..]);
|
||||
let first_block_hash = async_blake2sum(first_block.clone()).await;
|
||||
|
||||
let tx_result = (|| async {
|
||||
let (total_size, data_md5sum, data_sha256sum) = read_and_put_blocks(
|
||||
@@ -273,14 +280,23 @@ async fn read_and_put_blocks<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
|
||||
garage: &Garage,
|
||||
version: &Version,
|
||||
part_number: u64,
|
||||
first_block: Vec<u8>,
|
||||
first_block: Bytes,
|
||||
first_block_hash: Hash,
|
||||
chunker: &mut StreamChunker<S>,
|
||||
) -> Result<(u64, GenericArray<u8, typenum::U16>, Hash), Error> {
|
||||
let mut md5hasher = Md5::new();
|
||||
let mut sha256hasher = Sha256::new();
|
||||
md5hasher.update(&first_block[..]);
|
||||
sha256hasher.update(&first_block[..]);
|
||||
let tracer = opentelemetry::global::tracer("garage");
|
||||
|
||||
let md5hasher = AsyncHasher::<Md5>::new();
|
||||
let sha256hasher = AsyncHasher::<Sha256>::new();
|
||||
|
||||
futures::future::join(
|
||||
md5hasher.update(first_block.clone()),
|
||||
sha256hasher.update(first_block.clone()),
|
||||
)
|
||||
.with_context(Context::current_with_span(
|
||||
tracer.start("Hash first block (md5, sha256)"),
|
||||
))
|
||||
.await;
|
||||
|
||||
let mut next_offset = first_block.len();
|
||||
let mut put_curr_version_block = put_block_meta(
|
||||
@@ -302,9 +318,15 @@ async fn read_and_put_blocks<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
|
||||
chunker.next(),
|
||||
)?;
|
||||
if let Some(block) = next_block {
|
||||
md5hasher.update(&block[..]);
|
||||
sha256hasher.update(&block[..]);
|
||||
let block_hash = blake2sum(&block[..]);
|
||||
let (_, _, block_hash) = futures::future::join3(
|
||||
md5hasher.update(block.clone()),
|
||||
sha256hasher.update(block.clone()),
|
||||
async_blake2sum(block.clone()),
|
||||
)
|
||||
.with_context(Context::current_with_span(
|
||||
tracer.start("Hash block (md5, sha256, blake2)"),
|
||||
))
|
||||
.await;
|
||||
let block_len = block.len();
|
||||
put_curr_version_block = put_block_meta(
|
||||
garage,
|
||||
@@ -322,9 +344,9 @@ async fn read_and_put_blocks<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
|
||||
}
|
||||
|
||||
let total_size = next_offset as u64;
|
||||
let data_md5sum = md5hasher.finalize();
|
||||
let data_md5sum = md5hasher.finalize().await;
|
||||
|
||||
let data_sha256sum = sha256hasher.finalize();
|
||||
let data_sha256sum = sha256hasher.finalize().await;
|
||||
let data_sha256sum = Hash::try_from(&data_sha256sum[..]).unwrap();
|
||||
|
||||
Ok((total_size, data_md5sum, data_sha256sum))
|
||||
@@ -364,7 +386,7 @@ struct StreamChunker<S: Stream<Item = Result<Bytes, Error>>> {
|
||||
stream: S,
|
||||
read_all: bool,
|
||||
block_size: usize,
|
||||
buf: VecDeque<u8>,
|
||||
buf: BytesBuf,
|
||||
}
|
||||
|
||||
impl<S: Stream<Item = Result<Bytes, Error>> + Unpin> StreamChunker<S> {
|
||||
@@ -373,11 +395,11 @@ impl<S: Stream<Item = Result<Bytes, Error>> + Unpin> StreamChunker<S> {
|
||||
stream,
|
||||
read_all: false,
|
||||
block_size,
|
||||
buf: VecDeque::with_capacity(2 * block_size),
|
||||
buf: BytesBuf::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn next(&mut self) -> Result<Option<Vec<u8>>, Error> {
|
||||
async fn next(&mut self) -> Result<Option<Bytes>, Error> {
|
||||
while !self.read_all && self.buf.len() < self.block_size {
|
||||
if let Some(block) = self.stream.next().await {
|
||||
let bytes = block?;
|
||||
@@ -390,12 +412,8 @@ impl<S: Stream<Item = Result<Bytes, Error>> + Unpin> StreamChunker<S> {
|
||||
|
||||
if self.buf.is_empty() {
|
||||
Ok(None)
|
||||
} else if self.buf.len() <= self.block_size {
|
||||
let block = self.buf.drain(..).collect::<Vec<u8>>();
|
||||
Ok(Some(block))
|
||||
} else {
|
||||
let block = self.buf.drain(..self.block_size).collect::<Vec<u8>>();
|
||||
Ok(Some(block))
|
||||
Ok(Some(self.buf.take_max(self.block_size)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -504,7 +522,9 @@ pub async fn handle_put_part(
|
||||
|
||||
// Copy block to store
|
||||
let version = Version::new(version_uuid, bucket_id, key, false);
|
||||
let first_block_hash = blake2sum(&first_block[..]);
|
||||
|
||||
let first_block_hash = async_blake2sum(first_block.clone()).await;
|
||||
|
||||
let (_, data_md5sum, data_sha256sum) = read_and_put_blocks(
|
||||
&garage,
|
||||
&version,
|
||||
|
||||
+21
-21
@@ -25,7 +25,7 @@ impl From<&str> for Value {
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct IntValue(#[serde(rename = "$value")] pub i64);
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct Bucket {
|
||||
#[serde(rename = "CreationDate")]
|
||||
pub creation_date: Value,
|
||||
@@ -33,7 +33,7 @@ pub struct Bucket {
|
||||
pub name: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct Owner {
|
||||
#[serde(rename = "DisplayName")]
|
||||
pub display_name: Value,
|
||||
@@ -41,13 +41,13 @@ pub struct Owner {
|
||||
pub id: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct BucketList {
|
||||
#[serde(rename = "Bucket")]
|
||||
pub entries: Vec<Bucket>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct ListAllMyBucketsResult {
|
||||
#[serde(rename = "Buckets")]
|
||||
pub buckets: BucketList,
|
||||
@@ -55,7 +55,7 @@ pub struct ListAllMyBucketsResult {
|
||||
pub owner: Owner,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct LocationConstraint {
|
||||
#[serde(serialize_with = "xmlns_tag")]
|
||||
pub xmlns: (),
|
||||
@@ -63,7 +63,7 @@ pub struct LocationConstraint {
|
||||
pub region: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct Deleted {
|
||||
#[serde(rename = "Key")]
|
||||
pub key: Value,
|
||||
@@ -73,7 +73,7 @@ pub struct Deleted {
|
||||
pub delete_marker_version_id: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct Error {
|
||||
#[serde(rename = "Code")]
|
||||
pub code: Value,
|
||||
@@ -85,7 +85,7 @@ pub struct Error {
|
||||
pub region: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct DeleteError {
|
||||
#[serde(rename = "Code")]
|
||||
pub code: Value,
|
||||
@@ -97,7 +97,7 @@ pub struct DeleteError {
|
||||
pub version_id: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct DeleteResult {
|
||||
#[serde(serialize_with = "xmlns_tag")]
|
||||
pub xmlns: (),
|
||||
@@ -107,7 +107,7 @@ pub struct DeleteResult {
|
||||
pub errors: Vec<DeleteError>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct InitiateMultipartUploadResult {
|
||||
#[serde(serialize_with = "xmlns_tag")]
|
||||
pub xmlns: (),
|
||||
@@ -119,7 +119,7 @@ pub struct InitiateMultipartUploadResult {
|
||||
pub upload_id: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct CompleteMultipartUploadResult {
|
||||
#[serde(serialize_with = "xmlns_tag")]
|
||||
pub xmlns: (),
|
||||
@@ -133,7 +133,7 @@ pub struct CompleteMultipartUploadResult {
|
||||
pub etag: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct Initiator {
|
||||
#[serde(rename = "DisplayName")]
|
||||
pub display_name: Value,
|
||||
@@ -141,7 +141,7 @@ pub struct Initiator {
|
||||
pub id: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct ListMultipartItem {
|
||||
#[serde(rename = "Initiated")]
|
||||
pub initiated: Value,
|
||||
@@ -157,7 +157,7 @@ pub struct ListMultipartItem {
|
||||
pub storage_class: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct ListMultipartUploadsResult {
|
||||
#[serde(serialize_with = "xmlns_tag")]
|
||||
pub xmlns: (),
|
||||
@@ -187,7 +187,7 @@ pub struct ListMultipartUploadsResult {
|
||||
pub encoding_type: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct PartItem {
|
||||
#[serde(rename = "ETag")]
|
||||
pub etag: Value,
|
||||
@@ -199,7 +199,7 @@ pub struct PartItem {
|
||||
pub size: IntValue,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct ListPartsResult {
|
||||
#[serde(serialize_with = "xmlns_tag")]
|
||||
pub xmlns: (),
|
||||
@@ -227,7 +227,7 @@ pub struct ListPartsResult {
|
||||
pub storage_class: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct ListBucketItem {
|
||||
#[serde(rename = "Key")]
|
||||
pub key: Value,
|
||||
@@ -241,13 +241,13 @@ pub struct ListBucketItem {
|
||||
pub storage_class: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct CommonPrefix {
|
||||
#[serde(rename = "Prefix")]
|
||||
pub prefix: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct ListBucketResult {
|
||||
#[serde(serialize_with = "xmlns_tag")]
|
||||
pub xmlns: (),
|
||||
@@ -281,7 +281,7 @@ pub struct ListBucketResult {
|
||||
pub common_prefixes: Vec<CommonPrefix>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct VersioningConfiguration {
|
||||
#[serde(serialize_with = "xmlns_tag")]
|
||||
pub xmlns: (),
|
||||
@@ -289,7 +289,7 @@ pub struct VersioningConfiguration {
|
||||
pub status: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct PostObject {
|
||||
#[serde(serialize_with = "xmlns_tag")]
|
||||
pub xmlns: (),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use hmac::{Hmac, Mac, NewMac};
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
|
||||
use garage_util::data::{sha256sum, Hash};
|
||||
@@ -29,17 +29,17 @@ pub fn signing_hmac(
|
||||
secret_key: &str,
|
||||
region: &str,
|
||||
service: &str,
|
||||
) -> Result<HmacSha256, crypto_mac::InvalidKeyLength> {
|
||||
) -> Result<HmacSha256, crypto_common::InvalidLength> {
|
||||
let secret = String::from("AWS4") + secret_key;
|
||||
let mut date_hmac = HmacSha256::new_varkey(secret.as_bytes())?;
|
||||
let mut date_hmac = HmacSha256::new_from_slice(secret.as_bytes())?;
|
||||
date_hmac.update(datetime.format(SHORT_DATE).to_string().as_bytes());
|
||||
let mut region_hmac = HmacSha256::new_varkey(&date_hmac.finalize().into_bytes())?;
|
||||
let mut region_hmac = HmacSha256::new_from_slice(&date_hmac.finalize().into_bytes())?;
|
||||
region_hmac.update(region.as_bytes());
|
||||
let mut service_hmac = HmacSha256::new_varkey(®ion_hmac.finalize().into_bytes())?;
|
||||
let mut service_hmac = HmacSha256::new_from_slice(®ion_hmac.finalize().into_bytes())?;
|
||||
service_hmac.update(service.as_bytes());
|
||||
let mut signing_hmac = HmacSha256::new_varkey(&service_hmac.finalize().into_bytes())?;
|
||||
let mut signing_hmac = HmacSha256::new_from_slice(&service_hmac.finalize().into_bytes())?;
|
||||
signing_hmac.update(b"aws4_request");
|
||||
let hmac = HmacSha256::new_varkey(&signing_hmac.finalize().into_bytes())?;
|
||||
let hmac = HmacSha256::new_from_slice(&signing_hmac.finalize().into_bytes())?;
|
||||
Ok(hmac)
|
||||
}
|
||||
|
||||
|
||||
+10
-4
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_block"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
@@ -15,9 +15,9 @@ path = "lib.rs"
|
||||
|
||||
[dependencies]
|
||||
garage_db = { version = "0.8.0", path = "../db" }
|
||||
garage_rpc = { version = "0.7.0", path = "../rpc" }
|
||||
garage_util = { version = "0.7.0", path = "../util" }
|
||||
garage_table = { version = "0.7.0", path = "../table" }
|
||||
garage_rpc = { version = "0.8.0", path = "../rpc" }
|
||||
garage_util = { version = "0.8.0", path = "../util" }
|
||||
garage_table = { version = "0.8.0", path = "../table" }
|
||||
|
||||
opentelemetry = "0.17"
|
||||
|
||||
@@ -27,6 +27,8 @@ bytes = "1.0"
|
||||
hex = "0.4"
|
||||
tracing = "0.1.30"
|
||||
rand = "0.8"
|
||||
|
||||
async-compression = { version = "0.3", features = ["tokio", "zstd"] }
|
||||
zstd = { version = "0.9", default-features = false }
|
||||
|
||||
rmp-serde = "0.15"
|
||||
@@ -36,3 +38,7 @@ serde_bytes = "0.11"
|
||||
futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
||||
tokio-util = { version = "0.6", features = ["io"] }
|
||||
|
||||
[features]
|
||||
system-libs = [ "zstd/pkg-config" ]
|
||||
|
||||
+36
-12
@@ -1,16 +1,22 @@
|
||||
use bytes::Bytes;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zstd::stream::{decode_all as zstd_decode, Encoder};
|
||||
|
||||
use garage_util::data::*;
|
||||
use garage_util::error::*;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
pub enum DataBlockHeader {
|
||||
Plain,
|
||||
Compressed,
|
||||
}
|
||||
|
||||
/// A possibly compressed block of data
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum DataBlock {
|
||||
/// Uncompressed data
|
||||
Plain(#[serde(with = "serde_bytes")] Vec<u8>),
|
||||
Plain(Bytes),
|
||||
/// Data compressed with zstd
|
||||
Compressed(#[serde(with = "serde_bytes")] Vec<u8>),
|
||||
Compressed(Bytes),
|
||||
}
|
||||
|
||||
impl DataBlock {
|
||||
@@ -30,7 +36,7 @@ impl DataBlock {
|
||||
/// Get the buffer, possibly decompressing it, and verify it's integrity.
|
||||
/// For Plain block, data is compared to hash, for Compressed block, zstd checksumming system
|
||||
/// is used instead.
|
||||
pub fn verify_get(self, hash: Hash) -> Result<Vec<u8>, Error> {
|
||||
pub fn verify_get(self, hash: Hash) -> Result<Bytes, Error> {
|
||||
match self {
|
||||
DataBlock::Plain(data) => {
|
||||
if blake2sum(&data) == hash {
|
||||
@@ -39,9 +45,9 @@ impl DataBlock {
|
||||
Err(Error::CorruptData(hash))
|
||||
}
|
||||
}
|
||||
DataBlock::Compressed(data) => {
|
||||
zstd_decode(&data[..]).map_err(|_| Error::CorruptData(hash))
|
||||
}
|
||||
DataBlock::Compressed(data) => zstd_decode(&data[..])
|
||||
.map_err(|_| Error::CorruptData(hash))
|
||||
.map(Bytes::from),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,13 +67,31 @@ impl DataBlock {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_buffer(data: Vec<u8>, level: Option<i32>) -> DataBlock {
|
||||
if let Some(level) = level {
|
||||
if let Ok(data) = zstd_encode(&data[..], level) {
|
||||
return DataBlock::Compressed(data);
|
||||
pub async fn from_buffer(data: Bytes, level: Option<i32>) -> DataBlock {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
if let Some(level) = level {
|
||||
if let Ok(data) = zstd_encode(&data[..], level) {
|
||||
return DataBlock::Compressed(data.into());
|
||||
}
|
||||
}
|
||||
DataBlock::Plain(data)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn into_parts(self) -> (DataBlockHeader, Bytes) {
|
||||
match self {
|
||||
DataBlock::Plain(data) => (DataBlockHeader::Plain, data),
|
||||
DataBlock::Compressed(data) => (DataBlockHeader::Compressed, data),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_parts(h: DataBlockHeader, bytes: Bytes) -> Self {
|
||||
match h {
|
||||
DataBlockHeader::Plain => DataBlock::Plain(bytes),
|
||||
DataBlockHeader::Compressed => DataBlock::Compressed(bytes),
|
||||
}
|
||||
DataBlock::Plain(data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ extern crate tracing;
|
||||
|
||||
pub mod manager;
|
||||
pub mod repair;
|
||||
pub mod resync;
|
||||
|
||||
mod block;
|
||||
mod metrics;
|
||||
|
||||
+277
-560
File diff suppressed because it is too large
Load Diff
+28
-6
@@ -19,7 +19,17 @@ use garage_util::tranquilizer::Tranquilizer;
|
||||
|
||||
use crate::manager::*;
|
||||
|
||||
const SCRUB_INTERVAL: Duration = Duration::from_secs(3600 * 24 * 30); // full scrub every 30 days
|
||||
// Full scrub every 30 days
|
||||
const SCRUB_INTERVAL: Duration = Duration::from_secs(3600 * 24 * 30);
|
||||
// Scrub tranquility is initially set to 4, but can be changed in the CLI
|
||||
// and the updated version is persisted over Garage restarts
|
||||
const INITIAL_SCRUB_TRANQUILITY: u32 = 4;
|
||||
|
||||
// ---- ---- ----
|
||||
// FIRST KIND OF REPAIR: FINDING MISSING BLOCKS/USELESS BLOCKS
|
||||
// This is a one-shot repair operation that can be launched,
|
||||
// checks everything, and then exits.
|
||||
// ---- ---- ----
|
||||
|
||||
pub struct RepairWorker {
|
||||
manager: Arc<BlockManager>,
|
||||
@@ -102,7 +112,9 @@ impl Worker for RepairWorker {
|
||||
}
|
||||
|
||||
for hash in batch_of_hashes.into_iter() {
|
||||
self.manager.put_to_resync(&hash, Duration::from_secs(0))?;
|
||||
self.manager
|
||||
.resync
|
||||
.put_to_resync(&hash, Duration::from_secs(0))?;
|
||||
self.next_start = Some(hash)
|
||||
}
|
||||
|
||||
@@ -114,7 +126,9 @@ impl Worker for RepairWorker {
|
||||
// This allows us to find blocks we are storing but don't actually need,
|
||||
// so that we can offload them if necessary and then delete them locally.
|
||||
if let Some(hash) = bi.next().await? {
|
||||
self.manager.put_to_resync(&hash, Duration::from_secs(0))?;
|
||||
self.manager
|
||||
.resync
|
||||
.put_to_resync(&hash, Duration::from_secs(0))?;
|
||||
Ok(WorkerState::Busy)
|
||||
} else {
|
||||
Ok(WorkerState::Done)
|
||||
@@ -128,7 +142,13 @@ impl Worker for RepairWorker {
|
||||
}
|
||||
}
|
||||
|
||||
// ----
|
||||
// ---- ---- ----
|
||||
// SECOND KIND OF REPAIR: SCRUBBING THE DATASTORE
|
||||
// This is significantly more complex than the process above,
|
||||
// as it is a continuously-running task that triggers automatically
|
||||
// every SCRUB_INTERVAL, but can also be triggered manually
|
||||
// and whose parameter (esp. speed) can be controlled at runtime.
|
||||
// ---- ---- ----
|
||||
|
||||
pub struct ScrubWorker {
|
||||
manager: Arc<BlockManager>,
|
||||
@@ -176,7 +196,7 @@ impl ScrubWorker {
|
||||
Ok(v) => v,
|
||||
Err(_) => ScrubWorkerPersisted {
|
||||
time_last_complete_scrub: 0,
|
||||
tranquility: 4,
|
||||
tranquility: INITIAL_SCRUB_TRANQUILITY,
|
||||
corruptions_detected: 0,
|
||||
},
|
||||
};
|
||||
@@ -343,7 +363,9 @@ impl Worker for ScrubWorker {
|
||||
}
|
||||
}
|
||||
|
||||
// ----
|
||||
// ---- ---- ----
|
||||
// UTILITY FOR ENUMERATING THE BLOCK STORE
|
||||
// ---- ---- ----
|
||||
|
||||
struct BlockStoreIterator {
|
||||
path: Vec<ReadingDir>,
|
||||
|
||||
@@ -0,0 +1,589 @@
|
||||
use std::collections::HashSet;
|
||||
use std::convert::TryInto;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use tokio::select;
|
||||
use tokio::sync::{watch, Notify};
|
||||
|
||||
use opentelemetry::{
|
||||
trace::{FutureExt as OtelFutureExt, TraceContextExt, Tracer},
|
||||
Context, KeyValue,
|
||||
};
|
||||
|
||||
use garage_db as db;
|
||||
use garage_db::counted_tree_hack::CountedTree;
|
||||
|
||||
use garage_util::background::*;
|
||||
use garage_util::data::*;
|
||||
use garage_util::error::*;
|
||||
use garage_util::metrics::RecordDuration;
|
||||
use garage_util::persister::Persister;
|
||||
use garage_util::time::*;
|
||||
use garage_util::tranquilizer::Tranquilizer;
|
||||
|
||||
use garage_rpc::system::System;
|
||||
use garage_rpc::*;
|
||||
|
||||
use garage_table::replication::TableReplication;
|
||||
|
||||
use crate::manager::*;
|
||||
|
||||
// The delay between the time where a resync operation fails
|
||||
// and the time when it is retried, with exponential backoff
|
||||
// (multiplied by 2, 4, 8, 16, etc. for every consecutive failure).
|
||||
pub(crate) const RESYNC_RETRY_DELAY: Duration = Duration::from_secs(60);
|
||||
// The minimum retry delay is 60 seconds = 1 minute
|
||||
// The maximum retry delay is 60 seconds * 2^6 = 60 seconds << 6 = 64 minutes (~1 hour)
|
||||
pub(crate) const RESYNC_RETRY_DELAY_MAX_BACKOFF_POWER: u64 = 6;
|
||||
|
||||
// No more than 4 resync workers can be running in the system
|
||||
pub(crate) const MAX_RESYNC_WORKERS: usize = 4;
|
||||
// Resync tranquility is initially set to 2, but can be changed in the CLI
|
||||
// and the updated version is persisted over Garage restarts
|
||||
const INITIAL_RESYNC_TRANQUILITY: u32 = 2;
|
||||
|
||||
pub struct BlockResyncManager {
|
||||
pub(crate) queue: CountedTree,
|
||||
pub(crate) notify: Notify,
|
||||
pub(crate) errors: CountedTree,
|
||||
|
||||
busy_set: BusySet,
|
||||
|
||||
persister: Persister<ResyncPersistedConfig>,
|
||||
persisted: ArcSwap<ResyncPersistedConfig>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy)]
|
||||
struct ResyncPersistedConfig {
|
||||
n_workers: usize,
|
||||
tranquility: u32,
|
||||
}
|
||||
|
||||
enum ResyncIterResult {
|
||||
BusyDidSomething,
|
||||
BusyDidNothing,
|
||||
IdleFor(Duration),
|
||||
}
|
||||
|
||||
type BusySet = Arc<Mutex<HashSet<Vec<u8>>>>;
|
||||
|
||||
struct BusyBlock {
|
||||
time_bytes: Vec<u8>,
|
||||
hash_bytes: Vec<u8>,
|
||||
busy_set: BusySet,
|
||||
}
|
||||
|
||||
impl BlockResyncManager {
|
||||
pub(crate) fn new(db: &db::Db, system: &System) -> Self {
|
||||
let queue = db
|
||||
.open_tree("block_local_resync_queue")
|
||||
.expect("Unable to open block_local_resync_queue tree");
|
||||
let queue = CountedTree::new(queue).expect("Could not count block_local_resync_queue");
|
||||
|
||||
let errors = db
|
||||
.open_tree("block_local_resync_errors")
|
||||
.expect("Unable to open block_local_resync_errors tree");
|
||||
let errors = CountedTree::new(errors).expect("Could not count block_local_resync_errors");
|
||||
|
||||
let persister = Persister::new(&system.metadata_dir, "resync_cfg");
|
||||
let persisted = match persister.load() {
|
||||
Ok(v) => v,
|
||||
Err(_) => ResyncPersistedConfig {
|
||||
n_workers: 1,
|
||||
tranquility: INITIAL_RESYNC_TRANQUILITY,
|
||||
},
|
||||
};
|
||||
|
||||
Self {
|
||||
queue,
|
||||
notify: Notify::new(),
|
||||
errors,
|
||||
busy_set: Arc::new(Mutex::new(HashSet::new())),
|
||||
persister,
|
||||
persisted: ArcSwap::new(Arc::new(persisted)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get lenght of resync queue
|
||||
pub fn queue_len(&self) -> Result<usize, Error> {
|
||||
// This currently can't return an error because the CountedTree hack
|
||||
// doesn't error on .len(), but this will change when we remove the hack
|
||||
// (hopefully someday!)
|
||||
Ok(self.queue.len())
|
||||
}
|
||||
|
||||
/// Get number of blocks that have an error
|
||||
pub fn errors_len(&self) -> Result<usize, Error> {
|
||||
// (see queue_len comment)
|
||||
Ok(self.errors.len())
|
||||
}
|
||||
|
||||
// ---- Resync loop ----
|
||||
|
||||
// This part manages a queue of blocks that need to be
|
||||
// "resynchronized", i.e. that need to have a check that
|
||||
// they are at present if we need them, or that they are
|
||||
// deleted once the garbage collection delay has passed.
|
||||
//
|
||||
// Here are some explanations on how the resync queue works.
|
||||
// There are two Sled trees that are used to have information
|
||||
// about the status of blocks that need to be resynchronized:
|
||||
//
|
||||
// - resync.queue: a tree that is ordered first by a timestamp
|
||||
// (in milliseconds since Unix epoch) that is the time at which
|
||||
// the resync must be done, and second by block hash.
|
||||
// The key in this tree is just:
|
||||
// concat(timestamp (8 bytes), hash (32 bytes))
|
||||
// The value is the same 32-byte hash.
|
||||
//
|
||||
// - resync.errors: a tree that indicates for each block
|
||||
// if the last resync resulted in an error, and if so,
|
||||
// the following two informations (see the ErrorCounter struct):
|
||||
// - how many consecutive resync errors for this block?
|
||||
// - when was the last try?
|
||||
// These two informations are used to implement an
|
||||
// exponential backoff retry strategy.
|
||||
// The key in this tree is the 32-byte hash of the block,
|
||||
// and the value is the encoded ErrorCounter value.
|
||||
//
|
||||
// We need to have these two trees, because the resync queue
|
||||
// is not just a queue of items to process, but a set of items
|
||||
// that are waiting a specific delay until we can process them
|
||||
// (the delay being necessary both internally for the exponential
|
||||
// backoff strategy, and exposed as a parameter when adding items
|
||||
// to the queue, e.g. to wait until the GC delay has passed).
|
||||
// This is why we need one tree ordered by time, and one
|
||||
// ordered by identifier of item to be processed (block hash).
|
||||
//
|
||||
// When the worker wants to process an item it takes from
|
||||
// resync.queue, it checks in resync.errors that if there is an
|
||||
// exponential back-off delay to await, it has passed before we
|
||||
// process the item. If not, the item in the queue is skipped
|
||||
// (but added back for later processing after the time of the
|
||||
// delay).
|
||||
//
|
||||
// An alternative that would have seemed natural is to
|
||||
// only add items to resync.queue with a processing time that is
|
||||
// after the delay, but there are several issues with this:
|
||||
// - This requires to synchronize updates to resync.queue and
|
||||
// resync.errors (with the current model, there is only one thread,
|
||||
// the worker thread, that accesses resync.errors,
|
||||
// so no need to synchronize) by putting them both in a lock.
|
||||
// This would mean that block_incref might need to take a lock
|
||||
// before doing its thing, meaning it has much more chances of
|
||||
// not completing successfully if something bad happens to Garage.
|
||||
// Currently Garage is not able to recover from block_incref that
|
||||
// doesn't complete successfully, because it is necessary to ensure
|
||||
// the consistency between the state of the block manager and
|
||||
// information in the BlockRef table.
|
||||
// - If a resync fails, we put that block in the resync.errors table,
|
||||
// and also add it back to resync.queue to be processed after
|
||||
// the exponential back-off delay,
|
||||
// but maybe the block is already scheduled to be resynced again
|
||||
// at another time that is before the exponential back-off delay,
|
||||
// and we have no way to check that easily. This means that
|
||||
// in all cases, we need to check the resync.errors table
|
||||
// in the resync loop at the time when a block is popped from
|
||||
// the resync.queue.
|
||||
// Overall, the current design is therefore simpler and more robust
|
||||
// because it tolerates inconsistencies between the resync.queue
|
||||
// and resync.errors table (items being scheduled in resync.queue
|
||||
// for times that are earlier than the exponential back-off delay
|
||||
// is a natural condition that is handled properly).
|
||||
|
||||
pub(crate) fn put_to_resync(&self, hash: &Hash, delay: Duration) -> db::Result<()> {
|
||||
let when = now_msec() + delay.as_millis() as u64;
|
||||
self.put_to_resync_at(hash, when)
|
||||
}
|
||||
|
||||
pub(crate) fn put_to_resync_at(&self, hash: &Hash, when: u64) -> db::Result<()> {
|
||||
trace!("Put resync_queue: {} {:?}", when, hash);
|
||||
let mut key = u64::to_be_bytes(when).to_vec();
|
||||
key.extend(hash.as_ref());
|
||||
self.queue.insert(key, hash.as_ref())?;
|
||||
self.notify.notify_waiters();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn resync_iter(&self, manager: &BlockManager) -> Result<ResyncIterResult, db::Error> {
|
||||
if let Some(block) = self.get_block_to_resync()? {
|
||||
let time_msec = u64::from_be_bytes(block.time_bytes[0..8].try_into().unwrap());
|
||||
let now = now_msec();
|
||||
|
||||
if now >= time_msec {
|
||||
let hash = Hash::try_from(&block.hash_bytes[..]).unwrap();
|
||||
|
||||
if let Some(ec) = self.errors.get(hash.as_slice())? {
|
||||
let ec = ErrorCounter::decode(&ec);
|
||||
if now < ec.next_try() {
|
||||
// if next retry after an error is not yet,
|
||||
// don't do resync and return early, but still
|
||||
// make sure the item is still in queue at expected time
|
||||
self.put_to_resync_at(&hash, ec.next_try())?;
|
||||
// ec.next_try() > now >= time_msec, so this remove
|
||||
// is not removing the one we added just above
|
||||
// (we want to do the remove after the insert to ensure
|
||||
// that the item is not lost if we crash in-between)
|
||||
self.queue.remove(&block.time_bytes)?;
|
||||
return Ok(ResyncIterResult::BusyDidNothing);
|
||||
}
|
||||
}
|
||||
|
||||
let tracer = opentelemetry::global::tracer("garage");
|
||||
let trace_id = gen_uuid();
|
||||
let span = tracer
|
||||
.span_builder("Resync block")
|
||||
.with_trace_id(
|
||||
opentelemetry::trace::TraceId::from_hex(&hex::encode(
|
||||
&trace_id.as_slice()[..16],
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.with_attributes(vec![KeyValue::new("block", format!("{:?}", hash))])
|
||||
.start(&tracer);
|
||||
|
||||
let res = self
|
||||
.resync_block(manager, &hash)
|
||||
.with_context(Context::current_with_span(span))
|
||||
.bound_record_duration(&manager.metrics.resync_duration)
|
||||
.await;
|
||||
|
||||
manager.metrics.resync_counter.add(1);
|
||||
|
||||
if let Err(e) = &res {
|
||||
manager.metrics.resync_error_counter.add(1);
|
||||
warn!("Error when resyncing {:?}: {}", hash, e);
|
||||
|
||||
let err_counter = match self.errors.get(hash.as_slice())? {
|
||||
Some(ec) => ErrorCounter::decode(&ec).add1(now + 1),
|
||||
None => ErrorCounter::new(now + 1),
|
||||
};
|
||||
|
||||
self.errors.insert(hash.as_slice(), err_counter.encode())?;
|
||||
|
||||
self.put_to_resync_at(&hash, err_counter.next_try())?;
|
||||
// err_counter.next_try() >= now + 1 > now,
|
||||
// the entry we remove from the queue is not
|
||||
// the entry we inserted with put_to_resync_at
|
||||
self.queue.remove(&block.time_bytes)?;
|
||||
} else {
|
||||
self.errors.remove(hash.as_slice())?;
|
||||
self.queue.remove(&block.time_bytes)?;
|
||||
}
|
||||
|
||||
Ok(ResyncIterResult::BusyDidSomething)
|
||||
} else {
|
||||
Ok(ResyncIterResult::IdleFor(Duration::from_millis(
|
||||
time_msec - now,
|
||||
)))
|
||||
}
|
||||
} else {
|
||||
// Here we wait either for a notification that an item has been
|
||||
// added to the queue, or for a constant delay of 10 secs to expire.
|
||||
// The delay avoids a race condition where the notification happens
|
||||
// between the time we checked the queue and the first poll
|
||||
// to resync_notify.notified(): if that happens, we'll just loop
|
||||
// back 10 seconds later, which is fine.
|
||||
Ok(ResyncIterResult::IdleFor(Duration::from_secs(10)))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_block_to_resync(&self) -> Result<Option<BusyBlock>, db::Error> {
|
||||
let mut busy = self.busy_set.lock().unwrap();
|
||||
for it in self.queue.iter()? {
|
||||
let (time_bytes, hash_bytes) = it?;
|
||||
if !busy.contains(&time_bytes) {
|
||||
busy.insert(time_bytes.clone());
|
||||
return Ok(Some(BusyBlock {
|
||||
time_bytes,
|
||||
hash_bytes,
|
||||
busy_set: self.busy_set.clone(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn resync_block(&self, manager: &BlockManager, hash: &Hash) -> Result<(), Error> {
|
||||
let BlockStatus { exists, needed } = manager.check_block_status(hash).await?;
|
||||
|
||||
if exists != needed.is_needed() || exists != needed.is_nonzero() {
|
||||
debug!(
|
||||
"Resync block {:?}: exists {}, nonzero rc {}, deletable {}",
|
||||
hash,
|
||||
exists,
|
||||
needed.is_nonzero(),
|
||||
needed.is_deletable(),
|
||||
);
|
||||
}
|
||||
|
||||
if exists && needed.is_deletable() {
|
||||
info!("Resync block {:?}: offloading and deleting", hash);
|
||||
|
||||
let mut who = manager.replication.write_nodes(hash);
|
||||
if who.len() < manager.replication.write_quorum() {
|
||||
return Err(Error::Message("Not trying to offload block because we don't have a quorum of nodes to write to".to_string()));
|
||||
}
|
||||
who.retain(|id| *id != manager.system.id);
|
||||
|
||||
let who_needs_resps = manager
|
||||
.system
|
||||
.rpc
|
||||
.call_many(
|
||||
&manager.endpoint,
|
||||
&who,
|
||||
BlockRpc::NeedBlockQuery(*hash),
|
||||
RequestStrategy::with_priority(PRIO_BACKGROUND),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut need_nodes = vec![];
|
||||
for (node, needed) in who_needs_resps {
|
||||
match needed.err_context("NeedBlockQuery RPC")? {
|
||||
BlockRpc::NeedBlockReply(needed) => {
|
||||
if needed {
|
||||
need_nodes.push(node);
|
||||
}
|
||||
}
|
||||
m => {
|
||||
return Err(Error::unexpected_rpc_message(m));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !need_nodes.is_empty() {
|
||||
trace!(
|
||||
"Block {:?} needed by {} nodes, sending",
|
||||
hash,
|
||||
need_nodes.len()
|
||||
);
|
||||
|
||||
for node in need_nodes.iter() {
|
||||
manager
|
||||
.metrics
|
||||
.resync_send_counter
|
||||
.add(1, &[KeyValue::new("to", format!("{:?}", node))]);
|
||||
}
|
||||
|
||||
let block = manager.read_block(hash).await?;
|
||||
let (header, bytes) = block.into_parts();
|
||||
let put_block_message = Req::new(BlockRpc::PutBlock {
|
||||
hash: *hash,
|
||||
header,
|
||||
})?
|
||||
.with_stream_from_buffer(bytes);
|
||||
manager
|
||||
.system
|
||||
.rpc
|
||||
.try_call_many(
|
||||
&manager.endpoint,
|
||||
&need_nodes[..],
|
||||
put_block_message,
|
||||
RequestStrategy::with_priority(PRIO_BACKGROUND)
|
||||
.with_quorum(need_nodes.len()),
|
||||
)
|
||||
.await
|
||||
.err_context("PutBlock RPC")?;
|
||||
}
|
||||
info!(
|
||||
"Deleting unneeded block {:?}, offload finished ({} / {})",
|
||||
hash,
|
||||
need_nodes.len(),
|
||||
who.len()
|
||||
);
|
||||
|
||||
manager.delete_if_unneeded(hash).await?;
|
||||
|
||||
manager.rc.clear_deleted_block_rc(hash)?;
|
||||
}
|
||||
|
||||
if needed.is_nonzero() && !exists {
|
||||
info!(
|
||||
"Resync block {:?}: fetching absent but needed block (refcount > 0)",
|
||||
hash
|
||||
);
|
||||
|
||||
let block_data = manager.rpc_get_raw_block(hash, None).await?;
|
||||
|
||||
manager.metrics.resync_recv_counter.add(1);
|
||||
|
||||
manager.write_block(hash, &block_data).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_persisted(
|
||||
&self,
|
||||
update: impl Fn(&mut ResyncPersistedConfig),
|
||||
) -> Result<(), Error> {
|
||||
let mut cfg: ResyncPersistedConfig = *self.persisted.load().as_ref();
|
||||
update(&mut cfg);
|
||||
self.persister.save_async(&cfg).await?;
|
||||
self.persisted.store(Arc::new(cfg));
|
||||
self.notify.notify_waiters();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_n_workers(&self, n_workers: usize) -> Result<(), Error> {
|
||||
if !(1..=MAX_RESYNC_WORKERS).contains(&n_workers) {
|
||||
return Err(Error::Message(format!(
|
||||
"Invalid number of resync workers, must be between 1 and {}",
|
||||
MAX_RESYNC_WORKERS
|
||||
)));
|
||||
}
|
||||
self.update_persisted(|cfg| cfg.n_workers = n_workers).await
|
||||
}
|
||||
|
||||
pub async fn set_tranquility(&self, tranquility: u32) -> Result<(), Error> {
|
||||
self.update_persisted(|cfg| cfg.tranquility = tranquility)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for BusyBlock {
|
||||
fn drop(&mut self) {
|
||||
let mut busy = self.busy_set.lock().unwrap();
|
||||
busy.remove(&self.time_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ResyncWorker {
|
||||
index: usize,
|
||||
manager: Arc<BlockManager>,
|
||||
tranquilizer: Tranquilizer,
|
||||
next_delay: Duration,
|
||||
}
|
||||
|
||||
impl ResyncWorker {
|
||||
pub(crate) fn new(index: usize, manager: Arc<BlockManager>) -> Self {
|
||||
Self {
|
||||
index,
|
||||
manager,
|
||||
tranquilizer: Tranquilizer::new(30),
|
||||
next_delay: Duration::from_secs(10),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Worker for ResyncWorker {
|
||||
fn name(&self) -> String {
|
||||
format!("Block resync worker #{}", self.index + 1)
|
||||
}
|
||||
|
||||
fn info(&self) -> Option<String> {
|
||||
let persisted = self.manager.resync.persisted.load();
|
||||
|
||||
if self.index >= persisted.n_workers {
|
||||
return Some("(unused)".into());
|
||||
}
|
||||
|
||||
let mut ret = vec![];
|
||||
ret.push(format!("tranquility = {}", persisted.tranquility));
|
||||
|
||||
let qlen = self.manager.resync.queue_len().unwrap_or(0);
|
||||
if qlen > 0 {
|
||||
ret.push(format!("{} blocks in queue", qlen));
|
||||
}
|
||||
|
||||
let elen = self.manager.resync.errors_len().unwrap_or(0);
|
||||
if elen > 0 {
|
||||
ret.push(format!("{} blocks in error state", elen));
|
||||
}
|
||||
|
||||
Some(ret.join(", "))
|
||||
}
|
||||
|
||||
async fn work(&mut self, _must_exit: &mut watch::Receiver<bool>) -> Result<WorkerState, Error> {
|
||||
if self.index >= self.manager.resync.persisted.load().n_workers {
|
||||
return Ok(WorkerState::Idle);
|
||||
}
|
||||
|
||||
self.tranquilizer.reset();
|
||||
match self.manager.resync.resync_iter(&self.manager).await {
|
||||
Ok(ResyncIterResult::BusyDidSomething) => Ok(self
|
||||
.tranquilizer
|
||||
.tranquilize_worker(self.manager.resync.persisted.load().tranquility)),
|
||||
Ok(ResyncIterResult::BusyDidNothing) => Ok(WorkerState::Busy),
|
||||
Ok(ResyncIterResult::IdleFor(delay)) => {
|
||||
self.next_delay = delay;
|
||||
Ok(WorkerState::Idle)
|
||||
}
|
||||
Err(e) => {
|
||||
// The errors that we have here are only Sled errors
|
||||
// We don't really know how to handle them so just ¯\_(ツ)_/¯
|
||||
// (there is kind of an assumption that Sled won't error on us,
|
||||
// if it does there is not much we can do -- TODO should we just panic?)
|
||||
// Here we just give the error to the worker manager,
|
||||
// it will print it to the logs and increment a counter
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_work(&mut self, _must_exit: &watch::Receiver<bool>) -> WorkerState {
|
||||
while self.index >= self.manager.resync.persisted.load().n_workers {
|
||||
self.manager.resync.notify.notified().await
|
||||
}
|
||||
|
||||
select! {
|
||||
_ = tokio::time::sleep(self.next_delay) => (),
|
||||
_ = self.manager.resync.notify.notified() => (),
|
||||
};
|
||||
|
||||
WorkerState::Busy
|
||||
}
|
||||
}
|
||||
|
||||
/// Counts the number of errors when resyncing a block,
|
||||
/// and the time of the last try.
|
||||
/// Used to implement exponential backoff.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct ErrorCounter {
|
||||
errors: u64,
|
||||
last_try: u64,
|
||||
}
|
||||
|
||||
impl ErrorCounter {
|
||||
fn new(now: u64) -> Self {
|
||||
Self {
|
||||
errors: 1,
|
||||
last_try: now,
|
||||
}
|
||||
}
|
||||
|
||||
fn decode(data: &[u8]) -> Self {
|
||||
Self {
|
||||
errors: u64::from_be_bytes(data[0..8].try_into().unwrap()),
|
||||
last_try: u64::from_be_bytes(data[8..16].try_into().unwrap()),
|
||||
}
|
||||
}
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
[
|
||||
u64::to_be_bytes(self.errors),
|
||||
u64::to_be_bytes(self.last_try),
|
||||
]
|
||||
.concat()
|
||||
}
|
||||
|
||||
fn add1(self, now: u64) -> Self {
|
||||
Self {
|
||||
errors: self.errors + 1,
|
||||
last_try: now,
|
||||
}
|
||||
}
|
||||
|
||||
fn delay_msec(&self) -> u64 {
|
||||
(RESYNC_RETRY_DELAY.as_millis() as u64)
|
||||
<< std::cmp::min(self.errors - 1, RESYNC_RETRY_DELAY_MAX_BACKOFF_POWER)
|
||||
}
|
||||
fn next_try(&self) -> u64 {
|
||||
self.last_try + self.delay_msec()
|
||||
}
|
||||
}
|
||||
+6
-3
@@ -21,9 +21,9 @@ err-derive = "0.3"
|
||||
hexdump = "0.1"
|
||||
tracing = "0.1.30"
|
||||
|
||||
heed = "0.11"
|
||||
rusqlite = { version = "0.27", features = ["bundled"] }
|
||||
sled = "0.34"
|
||||
heed = { version = "0.11", default-features = false, features = ["lmdb"], optional = true }
|
||||
rusqlite = { version = "0.27", optional = true }
|
||||
sled = { version = "0.34", optional = true }
|
||||
|
||||
# cli deps
|
||||
clap = { version = "3.1.18", optional = true, features = ["derive", "env"] }
|
||||
@@ -33,4 +33,7 @@ pretty_env_logger = { version = "0.4", optional = true }
|
||||
mktemp = "0.4"
|
||||
|
||||
[features]
|
||||
bundled-libs = [ "rusqlite/bundled" ]
|
||||
cli = ["clap", "pretty_env_logger"]
|
||||
lmdb = [ "heed" ]
|
||||
sqlite = [ "rusqlite" ]
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
#[macro_use]
|
||||
#[cfg(feature = "sqlite")]
|
||||
extern crate tracing;
|
||||
|
||||
#[cfg(not(any(feature = "lmdb", feature = "sled", feature = "sqlite")))]
|
||||
compile_error!("Must activate the Cargo feature for at least one DB engine: lmdb, sled or sqlite.");
|
||||
|
||||
#[cfg(feature = "lmdb")]
|
||||
pub mod lmdb_adapter;
|
||||
#[cfg(feature = "sled")]
|
||||
pub mod sled_adapter;
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub mod sqlite_adapter;
|
||||
|
||||
pub mod counted_tree_hack;
|
||||
|
||||
@@ -345,7 +345,6 @@ pub fn recommended_map_size() -> usize {
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
pub fn recommended_map_size() -> usize {
|
||||
use log::warn;
|
||||
warn!("LMDB is not recommended on 32-bit systems, database size will be limited");
|
||||
1usize << 30
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ use std::pin::Pin;
|
||||
use std::ptr::NonNull;
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
|
||||
use tracing::trace;
|
||||
|
||||
use rusqlite::{params, Connection, Rows, Statement, Transaction};
|
||||
|
||||
use crate::{
|
||||
|
||||
+37
-18
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
@@ -22,20 +22,20 @@ path = "tests/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
garage_db = { version = "0.8.0", path = "../db" }
|
||||
garage_api = { version = "0.7.0", path = "../api" }
|
||||
garage_block = { version = "0.7.0", path = "../block" }
|
||||
garage_model = { version = "0.7.0", path = "../model" }
|
||||
garage_rpc = { version = "0.7.0", path = "../rpc" }
|
||||
garage_table = { version = "0.7.0", path = "../table" }
|
||||
garage_util = { version = "0.7.0", path = "../util" }
|
||||
garage_web = { version = "0.7.0", path = "../web" }
|
||||
garage_api = { version = "0.8.0", path = "../api" }
|
||||
garage_block = { version = "0.8.0", path = "../block" }
|
||||
garage_model = { version = "0.8.0", path = "../model" }
|
||||
garage_rpc = { version = "0.8.0", path = "../rpc" }
|
||||
garage_table = { version = "0.8.0", path = "../table" }
|
||||
garage_util = { version = "0.8.0", path = "../util" }
|
||||
garage_web = { version = "0.8.0", path = "../web" }
|
||||
|
||||
bytes = "1.0"
|
||||
bytesize = "1.1"
|
||||
timeago = "0.3"
|
||||
hex = "0.4"
|
||||
tracing = { version = "0.1.30", features = ["log-always"] }
|
||||
pretty_env_logger = "0.4"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
rand = "0.8"
|
||||
async-trait = "0.1.7"
|
||||
sodiumoxide = { version = "0.2.5-0", package = "kuska-sodiumoxide" }
|
||||
@@ -50,22 +50,20 @@ futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
||||
|
||||
#netapp = { version = "0.3.2", git = "https://git.deuxfleurs.fr/lx/netapp" }
|
||||
#netapp = { version = "0.4", path = "../../../netapp" }
|
||||
netapp = "0.4"
|
||||
netapp = "0.5"
|
||||
|
||||
opentelemetry = { version = "0.17", features = [ "rt-tokio" ] }
|
||||
opentelemetry-prometheus = "0.10"
|
||||
opentelemetry-otlp = "0.10"
|
||||
prometheus = "0.13"
|
||||
opentelemetry-prometheus = { version = "0.10", optional = true }
|
||||
opentelemetry-otlp = { version = "0.10", optional = true }
|
||||
prometheus = { version = "0.13", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
aws-sdk-s3 = "0.8"
|
||||
chrono = "0.4"
|
||||
http = "0.2"
|
||||
hmac = "0.10"
|
||||
hmac = "0.12"
|
||||
hyper = { version = "0.14", features = ["client", "http1", "runtime"] }
|
||||
sha2 = "0.9"
|
||||
sha2 = "0.10"
|
||||
|
||||
static_init = "1.0"
|
||||
assert-json-diff = "2.0"
|
||||
@@ -74,5 +72,26 @@ base64 = "0.13"
|
||||
|
||||
|
||||
[features]
|
||||
kubernetes-discovery = [ "garage_rpc/kubernetes-discovery" ]
|
||||
default = [ "bundled-libs", "metrics", "sled" ]
|
||||
|
||||
k2v = [ "garage_util/k2v", "garage_api/k2v" ]
|
||||
|
||||
# Database engines, Sled is still our default even though we don't like it
|
||||
sled = [ "garage_model/sled" ]
|
||||
lmdb = [ "garage_model/lmdb" ]
|
||||
sqlite = [ "garage_model/sqlite" ]
|
||||
|
||||
# Automatic registration and discovery via Kubernetes API
|
||||
kubernetes-discovery = [ "garage_rpc/kubernetes-discovery" ]
|
||||
# Prometheus exporter (/metrics endpoint).
|
||||
metrics = [ "garage_api/metrics", "opentelemetry-prometheus", "prometheus" ]
|
||||
# Exporter for the OpenTelemetry Collector.
|
||||
telemetry-otlp = [ "opentelemetry-otlp" ]
|
||||
|
||||
# NOTE: bundled-libs and system-libs should be treat as mutually exclusive;
|
||||
# exactly one of them should be enabled.
|
||||
|
||||
# Use bundled libsqlite instead of linking against system-provided.
|
||||
bundled-libs = [ "garage_db/bundled-libs" ]
|
||||
# Link against system-provided libsodium and libzstd.
|
||||
system-libs = [ "garage_block/system-libs", "garage_rpc/system-libs", "sodiumoxide/use-pkg-config" ]
|
||||
|
||||
+37
-6
@@ -15,6 +15,8 @@ use garage_table::*;
|
||||
|
||||
use garage_rpc::*;
|
||||
|
||||
use garage_block::repair::ScrubWorkerCommand;
|
||||
|
||||
use garage_model::bucket_alias_table::*;
|
||||
use garage_model::bucket_table::*;
|
||||
use garage_model::garage::Garage;
|
||||
@@ -681,7 +683,7 @@ impl AdminRpcHandler {
|
||||
.endpoint
|
||||
.call(
|
||||
&node,
|
||||
&AdminRpc::LaunchRepair(opt_to_send.clone()),
|
||||
AdminRpc::LaunchRepair(opt_to_send.clone()),
|
||||
PRIO_NORMAL,
|
||||
)
|
||||
.await;
|
||||
@@ -721,7 +723,7 @@ impl AdminRpcHandler {
|
||||
let node_id = (*node).into();
|
||||
match self
|
||||
.endpoint
|
||||
.call(&node_id, &AdminRpc::Stats(opt), PRIO_NORMAL)
|
||||
.call(&node_id, AdminRpc::Stats(opt), PRIO_NORMAL)
|
||||
.await?
|
||||
{
|
||||
Ok(AdminRpc::Ok(s)) => writeln!(&mut ret, "{}", s).unwrap(),
|
||||
@@ -739,8 +741,11 @@ impl AdminRpcHandler {
|
||||
let mut ret = String::new();
|
||||
writeln!(
|
||||
&mut ret,
|
||||
"\nGarage version: {}",
|
||||
self.garage.system.garage_version(),
|
||||
"\nGarage version: {} [features: {}]",
|
||||
garage_util::version::garage_version(),
|
||||
garage_util::version::garage_features()
|
||||
.map(|list| list.join(", "))
|
||||
.unwrap_or_else(|| "(unknown)".into()),
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(&mut ret, "\nDatabase engine: {}", self.garage.db.engine()).unwrap();
|
||||
@@ -779,13 +784,13 @@ impl AdminRpcHandler {
|
||||
writeln!(
|
||||
&mut ret,
|
||||
" resync queue length: {}",
|
||||
self.garage.block_manager.resync_queue_len()?
|
||||
self.garage.block_manager.resync.queue_len()?
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
&mut ret,
|
||||
" blocks with resync errors: {}",
|
||||
self.garage.block_manager.resync_errors_len()?
|
||||
self.garage.block_manager.resync.errors_len()?
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -836,6 +841,32 @@ impl AdminRpcHandler {
|
||||
let workers = self.garage.background.get_worker_info();
|
||||
Ok(AdminRpc::WorkerList(workers, opt))
|
||||
}
|
||||
WorkerCmd::Set { opt } => match opt {
|
||||
WorkerSetCmd::ScrubTranquility { tranquility } => {
|
||||
let scrub_command = ScrubWorkerCommand::SetTranquility(tranquility);
|
||||
self.garage
|
||||
.block_manager
|
||||
.send_scrub_command(scrub_command)
|
||||
.await;
|
||||
Ok(AdminRpc::Ok("Scrub tranquility updated".into()))
|
||||
}
|
||||
WorkerSetCmd::ResyncNWorkers { n_workers } => {
|
||||
self.garage
|
||||
.block_manager
|
||||
.resync
|
||||
.set_n_workers(n_workers)
|
||||
.await?;
|
||||
Ok(AdminRpc::Ok("Number of resync workers updated".into()))
|
||||
}
|
||||
WorkerSetCmd::ResyncTranquility { tranquility } => {
|
||||
self.garage
|
||||
.block_manager
|
||||
.resync
|
||||
.set_tranquility(tranquility)
|
||||
.await?;
|
||||
Ok(AdminRpc::Ok("Resync tranquility updated".into()))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ pub async fn cli_command_dispatch(
|
||||
|
||||
pub async fn cmd_status(rpc_cli: &Endpoint<SystemRpc, ()>, rpc_host: NodeID) -> Result<(), Error> {
|
||||
let status = match rpc_cli
|
||||
.call(&rpc_host, &SystemRpc::GetKnownNodes, PRIO_NORMAL)
|
||||
.call(&rpc_host, SystemRpc::GetKnownNodes, PRIO_NORMAL)
|
||||
.await??
|
||||
{
|
||||
SystemRpc::ReturnKnownNodes(nodes) => nodes,
|
||||
@@ -149,7 +149,7 @@ pub async fn cmd_connect(
|
||||
args: ConnectNodeOpt,
|
||||
) -> Result<(), Error> {
|
||||
match rpc_cli
|
||||
.call(&rpc_host, &SystemRpc::Connect(args.node), PRIO_NORMAL)
|
||||
.call(&rpc_host, SystemRpc::Connect(args.node), PRIO_NORMAL)
|
||||
.await??
|
||||
{
|
||||
SystemRpc::Ok => {
|
||||
@@ -165,7 +165,7 @@ pub async fn cmd_admin(
|
||||
rpc_host: NodeID,
|
||||
args: AdminRpc,
|
||||
) -> Result<(), HelperError> {
|
||||
match rpc_cli.call(&rpc_host, &args, PRIO_NORMAL).await?? {
|
||||
match rpc_cli.call(&rpc_host, args, PRIO_NORMAL).await?? {
|
||||
AdminRpc::Ok(msg) => {
|
||||
println!("{}", msg);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ pub async fn cmd_assign_role(
|
||||
args: AssignRoleOpt,
|
||||
) -> Result<(), Error> {
|
||||
let status = match rpc_cli
|
||||
.call(&rpc_host, &SystemRpc::GetKnownNodes, PRIO_NORMAL)
|
||||
.call(&rpc_host, SystemRpc::GetKnownNodes, PRIO_NORMAL)
|
||||
.await??
|
||||
{
|
||||
SystemRpc::ReturnKnownNodes(nodes) => nodes,
|
||||
@@ -245,7 +245,7 @@ pub async fn fetch_layout(
|
||||
rpc_host: NodeID,
|
||||
) -> Result<ClusterLayout, Error> {
|
||||
match rpc_cli
|
||||
.call(&rpc_host, &SystemRpc::PullClusterLayout, PRIO_NORMAL)
|
||||
.call(&rpc_host, SystemRpc::PullClusterLayout, PRIO_NORMAL)
|
||||
.await??
|
||||
{
|
||||
SystemRpc::AdvertiseClusterLayout(t) => Ok(t),
|
||||
@@ -261,7 +261,7 @@ pub async fn send_layout(
|
||||
rpc_cli
|
||||
.call(
|
||||
&rpc_host,
|
||||
&SystemRpc::AdvertiseClusterLayout(layout),
|
||||
SystemRpc::AdvertiseClusterLayout(layout),
|
||||
PRIO_NORMAL,
|
||||
)
|
||||
.await??;
|
||||
|
||||
+71
-51
@@ -1,64 +1,65 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use structopt::StructOpt;
|
||||
|
||||
use garage_util::version::garage_version;
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
pub enum Command {
|
||||
/// Run Garage server
|
||||
#[structopt(name = "server")]
|
||||
#[structopt(name = "server", version = garage_version())]
|
||||
Server,
|
||||
|
||||
/// Get network status
|
||||
#[structopt(name = "status")]
|
||||
#[structopt(name = "status", version = garage_version())]
|
||||
Status,
|
||||
|
||||
/// Operations on individual Garage nodes
|
||||
#[structopt(name = "node")]
|
||||
#[structopt(name = "node", version = garage_version())]
|
||||
Node(NodeOperation),
|
||||
|
||||
/// Operations on the assignation of node roles in the cluster layout
|
||||
#[structopt(name = "layout")]
|
||||
#[structopt(name = "layout", version = garage_version())]
|
||||
Layout(LayoutOperation),
|
||||
|
||||
/// Operations on buckets
|
||||
#[structopt(name = "bucket")]
|
||||
#[structopt(name = "bucket", version = garage_version())]
|
||||
Bucket(BucketOperation),
|
||||
|
||||
/// Operations on S3 access keys
|
||||
#[structopt(name = "key")]
|
||||
#[structopt(name = "key", version = garage_version())]
|
||||
Key(KeyOperation),
|
||||
|
||||
/// Run migrations from previous Garage version
|
||||
/// (DO NOT USE WITHOUT READING FULL DOCUMENTATION)
|
||||
#[structopt(name = "migrate")]
|
||||
#[structopt(name = "migrate", version = garage_version())]
|
||||
Migrate(MigrateOpt),
|
||||
|
||||
/// Start repair of node data on remote node
|
||||
#[structopt(name = "repair")]
|
||||
#[structopt(name = "repair", version = garage_version())]
|
||||
Repair(RepairOpt),
|
||||
|
||||
/// Offline reparation of node data (these repairs must be run offline
|
||||
/// directly on the server node)
|
||||
#[structopt(name = "offline-repair")]
|
||||
#[structopt(name = "offline-repair", version = garage_version())]
|
||||
OfflineRepair(OfflineRepairOpt),
|
||||
|
||||
/// Gather node statistics
|
||||
#[structopt(name = "stats")]
|
||||
#[structopt(name = "stats", version = garage_version())]
|
||||
Stats(StatsOpt),
|
||||
|
||||
/// Manage background workers
|
||||
#[structopt(name = "worker")]
|
||||
#[structopt(name = "worker", version = garage_version())]
|
||||
Worker(WorkerOpt),
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
pub enum NodeOperation {
|
||||
/// Print identifier (public key) of this Garage node
|
||||
#[structopt(name = "id")]
|
||||
#[structopt(name = "id", version = garage_version())]
|
||||
NodeId(NodeIdOpt),
|
||||
|
||||
/// Connect to Garage node that is currently isolated from the system
|
||||
#[structopt(name = "connect")]
|
||||
#[structopt(name = "connect", version = garage_version())]
|
||||
Connect(ConnectNodeOpt),
|
||||
}
|
||||
|
||||
@@ -79,23 +80,23 @@ pub struct ConnectNodeOpt {
|
||||
#[derive(StructOpt, Debug)]
|
||||
pub enum LayoutOperation {
|
||||
/// Assign role to Garage node
|
||||
#[structopt(name = "assign")]
|
||||
#[structopt(name = "assign", version = garage_version())]
|
||||
Assign(AssignRoleOpt),
|
||||
|
||||
/// Remove role from Garage cluster node
|
||||
#[structopt(name = "remove")]
|
||||
#[structopt(name = "remove", version = garage_version())]
|
||||
Remove(RemoveRoleOpt),
|
||||
|
||||
/// Show roles currently assigned to nodes and changes staged for commit
|
||||
#[structopt(name = "show")]
|
||||
#[structopt(name = "show", version = garage_version())]
|
||||
Show,
|
||||
|
||||
/// Apply staged changes to cluster layout
|
||||
#[structopt(name = "apply")]
|
||||
#[structopt(name = "apply", version = garage_version())]
|
||||
Apply(ApplyLayoutOpt),
|
||||
|
||||
/// Revert staged changes to cluster layout
|
||||
#[structopt(name = "revert")]
|
||||
#[structopt(name = "revert", version = garage_version())]
|
||||
Revert(RevertLayoutOpt),
|
||||
}
|
||||
|
||||
@@ -150,43 +151,43 @@ pub struct RevertLayoutOpt {
|
||||
#[derive(Serialize, Deserialize, StructOpt, Debug)]
|
||||
pub enum BucketOperation {
|
||||
/// List buckets
|
||||
#[structopt(name = "list")]
|
||||
#[structopt(name = "list", version = garage_version())]
|
||||
List,
|
||||
|
||||
/// Get bucket info
|
||||
#[structopt(name = "info")]
|
||||
#[structopt(name = "info", version = garage_version())]
|
||||
Info(BucketOpt),
|
||||
|
||||
/// Create bucket
|
||||
#[structopt(name = "create")]
|
||||
#[structopt(name = "create", version = garage_version())]
|
||||
Create(BucketOpt),
|
||||
|
||||
/// Delete bucket
|
||||
#[structopt(name = "delete")]
|
||||
#[structopt(name = "delete", version = garage_version())]
|
||||
Delete(DeleteBucketOpt),
|
||||
|
||||
/// Alias bucket under new name
|
||||
#[structopt(name = "alias")]
|
||||
#[structopt(name = "alias", version = garage_version())]
|
||||
Alias(AliasBucketOpt),
|
||||
|
||||
/// Remove bucket alias
|
||||
#[structopt(name = "unalias")]
|
||||
#[structopt(name = "unalias", version = garage_version())]
|
||||
Unalias(UnaliasBucketOpt),
|
||||
|
||||
/// Allow key to read or write to bucket
|
||||
#[structopt(name = "allow")]
|
||||
#[structopt(name = "allow", version = garage_version())]
|
||||
Allow(PermBucketOpt),
|
||||
|
||||
/// Deny key from reading or writing to bucket
|
||||
#[structopt(name = "deny")]
|
||||
#[structopt(name = "deny", version = garage_version())]
|
||||
Deny(PermBucketOpt),
|
||||
|
||||
/// Expose as website or not
|
||||
#[structopt(name = "website")]
|
||||
#[structopt(name = "website", version = garage_version())]
|
||||
Website(WebsiteOpt),
|
||||
|
||||
/// Set the quotas for this bucket
|
||||
#[structopt(name = "set-quotas")]
|
||||
#[structopt(name = "set-quotas", version = garage_version())]
|
||||
SetQuotas(SetQuotasOpt),
|
||||
}
|
||||
|
||||
@@ -292,35 +293,35 @@ pub struct SetQuotasOpt {
|
||||
#[derive(Serialize, Deserialize, StructOpt, Debug)]
|
||||
pub enum KeyOperation {
|
||||
/// List keys
|
||||
#[structopt(name = "list")]
|
||||
#[structopt(name = "list", version = garage_version())]
|
||||
List,
|
||||
|
||||
/// Get key info
|
||||
#[structopt(name = "info")]
|
||||
#[structopt(name = "info", version = garage_version())]
|
||||
Info(KeyOpt),
|
||||
|
||||
/// Create new key
|
||||
#[structopt(name = "new")]
|
||||
#[structopt(name = "new", version = garage_version())]
|
||||
New(KeyNewOpt),
|
||||
|
||||
/// Rename key
|
||||
#[structopt(name = "rename")]
|
||||
#[structopt(name = "rename", version = garage_version())]
|
||||
Rename(KeyRenameOpt),
|
||||
|
||||
/// Delete key
|
||||
#[structopt(name = "delete")]
|
||||
#[structopt(name = "delete", version = garage_version())]
|
||||
Delete(KeyDeleteOpt),
|
||||
|
||||
/// Set permission flags for key
|
||||
#[structopt(name = "allow")]
|
||||
#[structopt(name = "allow", version = garage_version())]
|
||||
Allow(KeyPermOpt),
|
||||
|
||||
/// Unset permission flags for key
|
||||
#[structopt(name = "deny")]
|
||||
#[structopt(name = "deny", version = garage_version())]
|
||||
Deny(KeyPermOpt),
|
||||
|
||||
/// Import key
|
||||
#[structopt(name = "import")]
|
||||
#[structopt(name = "import", version = garage_version())]
|
||||
Import(KeyImportOpt),
|
||||
}
|
||||
|
||||
@@ -392,7 +393,7 @@ pub struct MigrateOpt {
|
||||
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
|
||||
pub enum MigrateWhat {
|
||||
/// Migrate buckets and permissions from v0.5.0
|
||||
#[structopt(name = "buckets050")]
|
||||
#[structopt(name = "buckets050", version = garage_version())]
|
||||
Buckets050,
|
||||
}
|
||||
|
||||
@@ -413,19 +414,19 @@ pub struct RepairOpt {
|
||||
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
|
||||
pub enum RepairWhat {
|
||||
/// Only do a full sync of metadata tables
|
||||
#[structopt(name = "tables")]
|
||||
#[structopt(name = "tables", version = garage_version())]
|
||||
Tables,
|
||||
/// Only repair (resync/rebalance) the set of stored blocks
|
||||
#[structopt(name = "blocks")]
|
||||
#[structopt(name = "blocks", version = garage_version())]
|
||||
Blocks,
|
||||
/// Only redo the propagation of object deletions to the version table (slow)
|
||||
#[structopt(name = "versions")]
|
||||
#[structopt(name = "versions", version = garage_version())]
|
||||
Versions,
|
||||
/// Only redo the propagation of version deletions to the block ref table (extremely slow)
|
||||
#[structopt(name = "block_refs")]
|
||||
#[structopt(name = "block_refs", version = garage_version())]
|
||||
BlockRefs,
|
||||
/// Verify integrity of all blocks on disc (extremely slow, i/o intensive)
|
||||
#[structopt(name = "scrub")]
|
||||
#[structopt(name = "scrub", version = garage_version())]
|
||||
Scrub {
|
||||
#[structopt(subcommand)]
|
||||
cmd: ScrubCmd,
|
||||
@@ -435,19 +436,19 @@ pub enum RepairWhat {
|
||||
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
|
||||
pub enum ScrubCmd {
|
||||
/// Start scrub
|
||||
#[structopt(name = "start")]
|
||||
#[structopt(name = "start", version = garage_version())]
|
||||
Start,
|
||||
/// Pause scrub (it will resume automatically after 24 hours)
|
||||
#[structopt(name = "pause")]
|
||||
#[structopt(name = "pause", version = garage_version())]
|
||||
Pause,
|
||||
/// Resume paused scrub
|
||||
#[structopt(name = "resume")]
|
||||
#[structopt(name = "resume", version = garage_version())]
|
||||
Resume,
|
||||
/// Cancel scrub in progress
|
||||
#[structopt(name = "cancel")]
|
||||
#[structopt(name = "cancel", version = garage_version())]
|
||||
Cancel,
|
||||
/// Set tranquility level for in-progress and future scrubs
|
||||
#[structopt(name = "set-tranquility")]
|
||||
#[structopt(name = "set-tranquility", version = garage_version())]
|
||||
SetTranquility {
|
||||
#[structopt()]
|
||||
tranquility: u32,
|
||||
@@ -468,10 +469,10 @@ pub struct OfflineRepairOpt {
|
||||
pub enum OfflineRepairWhat {
|
||||
/// Repair K2V item counters
|
||||
#[cfg(feature = "k2v")]
|
||||
#[structopt(name = "k2v_item_counters")]
|
||||
#[structopt(name = "k2v_item_counters", version = garage_version())]
|
||||
K2VItemCounters,
|
||||
/// Repair object counters
|
||||
#[structopt(name = "object_counters")]
|
||||
#[structopt(name = "object_counters", version = garage_version())]
|
||||
ObjectCounters,
|
||||
}
|
||||
|
||||
@@ -495,11 +496,17 @@ pub struct WorkerOpt {
|
||||
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
|
||||
pub enum WorkerCmd {
|
||||
/// List all workers on Garage node
|
||||
#[structopt(name = "list")]
|
||||
#[structopt(name = "list", version = garage_version())]
|
||||
List {
|
||||
#[structopt(flatten)]
|
||||
opt: WorkerListOpt,
|
||||
},
|
||||
/// Set worker parameter
|
||||
#[structopt(name = "set", version = garage_version())]
|
||||
Set {
|
||||
#[structopt(subcommand)]
|
||||
opt: WorkerSetCmd,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone, Copy)]
|
||||
@@ -511,3 +518,16 @@ pub struct WorkerListOpt {
|
||||
#[structopt(short = "e", long = "errors")]
|
||||
pub errors: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
|
||||
pub enum WorkerSetCmd {
|
||||
/// Set tranquility of scrub operations
|
||||
#[structopt(name = "scrub-tranquility", version = garage_version())]
|
||||
ScrubTranquility { tranquility: u32 },
|
||||
/// Set number of concurrent block resync workers
|
||||
#[structopt(name = "resync-n-workers", version = garage_version())]
|
||||
ResyncNWorkers { n_workers: usize },
|
||||
/// Set tranquility of block resync operations
|
||||
#[structopt(name = "resync-tranquility", version = garage_version())]
|
||||
ResyncTranquility { tranquility: u32 },
|
||||
}
|
||||
|
||||
+56
-4
@@ -8,8 +8,15 @@ mod admin;
|
||||
mod cli;
|
||||
mod repair;
|
||||
mod server;
|
||||
#[cfg(feature = "telemetry-otlp")]
|
||||
mod tracing_setup;
|
||||
|
||||
#[cfg(not(any(feature = "bundled-libs", feature = "system-libs")))]
|
||||
compile_error!("Either bundled-libs or system-libs Cargo feature must be enabled");
|
||||
|
||||
#[cfg(all(feature = "bundled-libs", feature = "system-libs"))]
|
||||
compile_error!("Only one of bundled-libs and system-libs Cargo features must be enabled");
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -29,7 +36,10 @@ use admin::*;
|
||||
use cli::*;
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(name = "garage")]
|
||||
#[structopt(
|
||||
name = "garage",
|
||||
about = "S3-compatible object store for self-hosted geo-distributed deployments"
|
||||
)]
|
||||
struct Opt {
|
||||
/// Host to connect to for admin operations, in the format:
|
||||
/// <public-key>@<ip>:<port>
|
||||
@@ -58,7 +68,10 @@ async fn main() {
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "netapp=info,garage=info")
|
||||
}
|
||||
pretty_env_logger::init();
|
||||
tracing_subscriber::fmt()
|
||||
.with_writer(std::io::stderr)
|
||||
.with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env())
|
||||
.init();
|
||||
sodiumoxide::init().expect("Unable to init sodiumoxide");
|
||||
|
||||
// Abort on panic (same behavior as in Go)
|
||||
@@ -67,7 +80,40 @@ async fn main() {
|
||||
std::process::abort();
|
||||
}));
|
||||
|
||||
let opt = Opt::from_args();
|
||||
// Initialize version and features info
|
||||
let features = &[
|
||||
#[cfg(feature = "k2v")]
|
||||
"k2v",
|
||||
#[cfg(feature = "sled")]
|
||||
"sled",
|
||||
#[cfg(feature = "lmdb")]
|
||||
"lmdb",
|
||||
#[cfg(feature = "sqlite")]
|
||||
"sqlite",
|
||||
#[cfg(feature = "kubernetes-discovery")]
|
||||
"kubernetes-discovery",
|
||||
#[cfg(feature = "metrics")]
|
||||
"metrics",
|
||||
#[cfg(feature = "telemetry-otlp")]
|
||||
"telemetry-otlp",
|
||||
#[cfg(feature = "bundled-libs")]
|
||||
"bundled-libs",
|
||||
#[cfg(feature = "system-libs")]
|
||||
"system-libs",
|
||||
][..];
|
||||
if let Some(git_version) = option_env!("GIT_VERSION") {
|
||||
garage_util::version::init_version(git_version);
|
||||
}
|
||||
garage_util::version::init_features(features);
|
||||
|
||||
// Parse arguments
|
||||
let version = format!(
|
||||
"{} [features: {}]",
|
||||
garage_util::version::garage_version(),
|
||||
features.join(", ")
|
||||
);
|
||||
let opt = Opt::from_clap(&Opt::clap().version(version.as_str()).get_matches());
|
||||
|
||||
let res = match opt.cmd {
|
||||
Command::Server => server::run_server(opt.config_file).await,
|
||||
Command::OfflineRepair(repair_opt) => {
|
||||
@@ -116,7 +162,13 @@ async fn cli_command(opt: Opt) -> Result<(), Error> {
|
||||
} else {
|
||||
let node_id = garage_rpc::system::read_node_id(&config.as_ref().unwrap().metadata_dir)
|
||||
.err_context(READ_KEY_ERROR)?;
|
||||
if let Some(a) = config.as_ref().and_then(|c| c.rpc_public_addr) {
|
||||
if let Some(a) = config.as_ref().and_then(|c| c.rpc_public_addr.as_ref()) {
|
||||
use std::net::ToSocketAddrs;
|
||||
let a = a
|
||||
.to_socket_addrs()
|
||||
.ok_or_message("unable to resolve rpc_public_addr specified in config file")?
|
||||
.next()
|
||||
.ok_or_message("unable to resolve rpc_public_addr specified in config file")?;
|
||||
(node_id, a)
|
||||
} else {
|
||||
let default_addr = SocketAddr::new(
|
||||
|
||||
+77
-44
@@ -9,12 +9,13 @@ use garage_util::error::Error;
|
||||
use garage_api::admin::api_server::AdminApiServer;
|
||||
use garage_api::s3::api_server::S3ApiServer;
|
||||
use garage_model::garage::Garage;
|
||||
use garage_web::run_web_server;
|
||||
use garage_web::WebServer;
|
||||
|
||||
#[cfg(feature = "k2v")]
|
||||
use garage_api::k2v::api_server::K2VApiServer;
|
||||
|
||||
use crate::admin::*;
|
||||
#[cfg(feature = "telemetry-otlp")]
|
||||
use crate::tracing_setup::*;
|
||||
|
||||
async fn wait_from(mut chan: watch::Receiver<bool>) {
|
||||
@@ -29,6 +30,8 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> {
|
||||
info!("Loading configuration...");
|
||||
let config = read_config(config_file)?;
|
||||
|
||||
// ---- Initialize Garage internals ----
|
||||
|
||||
info!("Initializing background runner...");
|
||||
let watch_cancel = netapp::util::watch_ctrl_c();
|
||||
let (background, await_background_done) = BackgroundRunner::new(16, watch_cancel.clone());
|
||||
@@ -36,9 +39,14 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> {
|
||||
info!("Initializing Garage main data store...");
|
||||
let garage = Garage::new(config.clone(), background)?;
|
||||
|
||||
info!("Initialize tracing...");
|
||||
if let Some(export_to) = config.admin.trace_sink {
|
||||
init_tracing(&export_to, garage.system.id)?;
|
||||
if config.admin.trace_sink.is_some() {
|
||||
info!("Initialize tracing...");
|
||||
|
||||
#[cfg(feature = "telemetry-otlp")]
|
||||
init_tracing(config.admin.trace_sink.as_ref().unwrap(), garage.system.id)?;
|
||||
|
||||
#[cfg(not(feature = "telemetry-otlp"))]
|
||||
error!("Garage was built without OTLP exporter, admin.trace_sink is ignored.");
|
||||
}
|
||||
|
||||
info!("Initialize Admin API server and metrics collector...");
|
||||
@@ -50,53 +58,78 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> {
|
||||
info!("Create admin RPC handler...");
|
||||
AdminRpcHandler::new(garage.clone());
|
||||
|
||||
info!("Initializing S3 API server...");
|
||||
let s3_api_server = tokio::spawn(S3ApiServer::run(
|
||||
garage.clone(),
|
||||
wait_from(watch_cancel.clone()),
|
||||
));
|
||||
// ---- Launch public-facing API servers ----
|
||||
|
||||
#[cfg(feature = "k2v")]
|
||||
let k2v_api_server = {
|
||||
info!("Initializing K2V API server...");
|
||||
tokio::spawn(K2VApiServer::run(
|
||||
garage.clone(),
|
||||
wait_from(watch_cancel.clone()),
|
||||
))
|
||||
};
|
||||
let mut servers = vec![];
|
||||
|
||||
info!("Initializing web server...");
|
||||
let web_server = tokio::spawn(run_web_server(
|
||||
garage.clone(),
|
||||
wait_from(watch_cancel.clone()),
|
||||
));
|
||||
if let Some(s3_bind_addr) = &config.s3_api.api_bind_addr {
|
||||
info!("Initializing S3 API server...");
|
||||
servers.push((
|
||||
"S3 API",
|
||||
tokio::spawn(S3ApiServer::run(
|
||||
garage.clone(),
|
||||
*s3_bind_addr,
|
||||
config.s3_api.s3_region.clone(),
|
||||
wait_from(watch_cancel.clone()),
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
||||
info!("Launching Admin API server...");
|
||||
let admin_server = tokio::spawn(admin_server.run(wait_from(watch_cancel.clone())));
|
||||
if config.k2v_api.is_some() {
|
||||
#[cfg(feature = "k2v")]
|
||||
{
|
||||
info!("Initializing K2V API server...");
|
||||
servers.push((
|
||||
"K2V API",
|
||||
tokio::spawn(K2VApiServer::run(
|
||||
garage.clone(),
|
||||
config.k2v_api.as_ref().unwrap().api_bind_addr,
|
||||
config.s3_api.s3_region.clone(),
|
||||
wait_from(watch_cancel.clone()),
|
||||
)),
|
||||
));
|
||||
}
|
||||
#[cfg(not(feature = "k2v"))]
|
||||
error!("K2V is not enabled in this build, cannot start K2V API server");
|
||||
}
|
||||
|
||||
if let Some(web_config) = &config.s3_web {
|
||||
info!("Initializing web server...");
|
||||
servers.push((
|
||||
"Web",
|
||||
tokio::spawn(WebServer::run(
|
||||
garage.clone(),
|
||||
web_config.bind_addr,
|
||||
web_config.root_domain.clone(),
|
||||
wait_from(watch_cancel.clone()),
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(admin_bind_addr) = &config.admin.api_bind_addr {
|
||||
info!("Launching Admin API server...");
|
||||
servers.push((
|
||||
"Admin",
|
||||
tokio::spawn(admin_server.run(*admin_bind_addr, wait_from(watch_cancel.clone()))),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "metrics"))]
|
||||
if config.admin.metrics_token.is_some() {
|
||||
warn!("This Garage version is built without the metrics feature");
|
||||
}
|
||||
|
||||
// Stuff runs
|
||||
|
||||
// When a cancel signal is sent, stuff stops
|
||||
if let Err(e) = s3_api_server.await? {
|
||||
warn!("S3 API server exited with error: {}", e);
|
||||
} else {
|
||||
info!("S3 API server exited without error.");
|
||||
}
|
||||
#[cfg(feature = "k2v")]
|
||||
if let Err(e) = k2v_api_server.await? {
|
||||
warn!("K2V API server exited with error: {}", e);
|
||||
} else {
|
||||
info!("K2V API server exited without error.");
|
||||
}
|
||||
if let Err(e) = web_server.await? {
|
||||
warn!("Web server exited with error: {}", e);
|
||||
} else {
|
||||
info!("Web server exited without error.");
|
||||
}
|
||||
if let Err(e) = admin_server.await? {
|
||||
warn!("Admin web server exited with error: {}", e);
|
||||
} else {
|
||||
info!("Admin API server exited without error.");
|
||||
|
||||
// Collect stuff
|
||||
for (desc, join_handle) in servers {
|
||||
if let Err(e) = join_handle.await? {
|
||||
error!("{} server exited with error: {}", desc, e);
|
||||
} else {
|
||||
info!("{} server exited without error.", desc);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove RPC handlers for system to break reference cycles
|
||||
|
||||
@@ -3,5 +3,8 @@ mod common;
|
||||
|
||||
mod admin;
|
||||
mod bucket;
|
||||
mod k2v;
|
||||
|
||||
mod s3;
|
||||
|
||||
#[cfg(feature = "k2v")]
|
||||
mod k2v;
|
||||
|
||||
@@ -263,4 +263,13 @@ async fn test_deleteobject() {
|
||||
.unwrap();
|
||||
|
||||
assert!(l.contents.is_none());
|
||||
|
||||
// Deleting a non-existing object shouldn't be a problem
|
||||
ctx.client
|
||||
.delete_object()
|
||||
.bucket(&bucket)
|
||||
.key("l-0")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ tokio = "1.17.0"
|
||||
|
||||
# cli deps
|
||||
clap = { version = "3.1.18", optional = true, features = ["derive", "env"] }
|
||||
garage_util = { version = "0.7.0", path = "../util", optional = true }
|
||||
garage_util = { version = "0.8.0", path = "../util", optional = true }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_model"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
@@ -15,11 +15,10 @@ path = "lib.rs"
|
||||
|
||||
[dependencies]
|
||||
garage_db = { version = "0.8.0", path = "../db" }
|
||||
garage_rpc = { version = "0.7.0", path = "../rpc" }
|
||||
garage_table = { version = "0.7.0", path = "../table" }
|
||||
garage_block = { version = "0.7.0", path = "../block" }
|
||||
garage_util = { version = "0.7.0", path = "../util" }
|
||||
garage_model_050 = { package = "garage_model", version = "0.5.1" }
|
||||
garage_rpc = { version = "0.8.0", path = "../rpc" }
|
||||
garage_table = { version = "0.8.0", path = "../table" }
|
||||
garage_block = { version = "0.8.0", path = "../block" }
|
||||
garage_util = { version = "0.8.0", path = "../util" }
|
||||
|
||||
async-trait = "0.1.7"
|
||||
arc-swap = "1.0"
|
||||
@@ -40,9 +39,10 @@ futures-util = "0.3"
|
||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
||||
opentelemetry = "0.17"
|
||||
|
||||
#netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
|
||||
#netapp = { version = "0.4", path = "../../../netapp" }
|
||||
netapp = "0.4"
|
||||
netapp = "0.5"
|
||||
|
||||
[features]
|
||||
k2v = [ "garage_util/k2v" ]
|
||||
lmdb = [ "garage_db/lmdb" ]
|
||||
sled = [ "garage_db/sled" ]
|
||||
sqlite = [ "garage_db/sqlite" ]
|
||||
|
||||
@@ -7,7 +7,7 @@ use garage_table::*;
|
||||
|
||||
/// The bucket alias table holds the names given to buckets
|
||||
/// in the global namespace.
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct BucketAlias {
|
||||
name: String,
|
||||
pub state: crdt::Lww<Option<Uuid>>,
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::permission::BucketKeyPerm;
|
||||
/// Its parameters are not directly accessible as:
|
||||
/// - It must be possible to merge paramaters, hence the use of a LWW CRDT.
|
||||
/// - A bucket has 2 states, Present or Deleted and parameters make sense only if present.
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Bucket {
|
||||
/// ID of the bucket
|
||||
pub id: Uuid,
|
||||
@@ -21,7 +21,7 @@ pub struct Bucket {
|
||||
}
|
||||
|
||||
/// Configuration for a bucket
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct BucketParams {
|
||||
/// Bucket's creation date
|
||||
pub creation_date: u64,
|
||||
|
||||
+46
-11
@@ -6,7 +6,7 @@ use garage_db as db;
|
||||
|
||||
use garage_util::background::*;
|
||||
use garage_util::config::*;
|
||||
use garage_util::error::Error;
|
||||
use garage_util::error::*;
|
||||
|
||||
use garage_rpc::system::System;
|
||||
|
||||
@@ -76,10 +76,17 @@ pub struct GarageK2V {
|
||||
impl Garage {
|
||||
/// Create and run garage
|
||||
pub fn new(config: Config, background: Arc<BackgroundRunner>) -> Result<Arc<Self>, Error> {
|
||||
// Create meta dir and data dir if they don't exist already
|
||||
std::fs::create_dir_all(&config.metadata_dir)
|
||||
.ok_or_message("Unable to create Garage metadata directory")?;
|
||||
std::fs::create_dir_all(&config.data_dir)
|
||||
.ok_or_message("Unable to create Garage data directory")?;
|
||||
|
||||
info!("Opening database...");
|
||||
let mut db_path = config.metadata_dir.clone();
|
||||
std::fs::create_dir_all(&db_path).expect("Unable to create Garage meta data directory");
|
||||
let db = match config.db_engine.as_str() {
|
||||
// ---- Sled DB ----
|
||||
#[cfg(feature = "sled")]
|
||||
"sled" => {
|
||||
db_path.push("db");
|
||||
info!("Opening Sled database at: {}", db_path.display());
|
||||
@@ -91,6 +98,10 @@ impl Garage {
|
||||
.expect("Unable to open sled DB");
|
||||
db::sled_adapter::SledDb::init(db)
|
||||
}
|
||||
#[cfg(not(feature = "sled"))]
|
||||
"sled" => return Err(Error::Message("sled db not available in this build".into())),
|
||||
// ---- Sqlite DB ----
|
||||
#[cfg(feature = "sqlite")]
|
||||
"sqlite" | "sqlite3" | "rusqlite" => {
|
||||
db_path.push("db.sqlite");
|
||||
info!("Opening Sqlite database at: {}", db_path.display());
|
||||
@@ -98,23 +109,48 @@ impl Garage {
|
||||
.expect("Unable to open sqlite DB");
|
||||
db::sqlite_adapter::SqliteDb::init(db)
|
||||
}
|
||||
#[cfg(not(feature = "sqlite"))]
|
||||
"sqlite" | "sqlite3" | "rusqlite" => {
|
||||
return Err(Error::Message(
|
||||
"sqlite db not available in this build".into(),
|
||||
))
|
||||
}
|
||||
// ---- LMDB DB ----
|
||||
#[cfg(feature = "lmdb")]
|
||||
"lmdb" | "heed" => {
|
||||
db_path.push("db.lmdb");
|
||||
info!("Opening LMDB database at: {}", db_path.display());
|
||||
std::fs::create_dir_all(&db_path).expect("Unable to create LMDB data directory");
|
||||
let map_size = garage_db::lmdb_adapter::recommended_map_size();
|
||||
|
||||
let db = db::lmdb_adapter::heed::EnvOpenOptions::new()
|
||||
.max_dbs(100)
|
||||
.map_size(map_size)
|
||||
.open(&db_path)
|
||||
.expect("Unable to open LMDB DB");
|
||||
use db::lmdb_adapter::heed;
|
||||
let mut env_builder = heed::EnvOpenOptions::new();
|
||||
env_builder.max_dbs(100);
|
||||
env_builder.max_readers(500);
|
||||
env_builder.map_size(map_size);
|
||||
unsafe {
|
||||
env_builder.flag(heed::flags::Flags::MdbNoSync);
|
||||
env_builder.flag(heed::flags::Flags::MdbNoMetaSync);
|
||||
}
|
||||
let db = env_builder.open(&db_path).expect("Unable to open LMDB DB");
|
||||
db::lmdb_adapter::LmdbDb::init(db)
|
||||
}
|
||||
#[cfg(not(feature = "lmdb"))]
|
||||
"lmdb" | "heed" => return Err(Error::Message("lmdb db not available in this build".into())),
|
||||
// ---- Unavailable DB engine ----
|
||||
e => {
|
||||
return Err(Error::Message(format!(
|
||||
"Unsupported DB engine: {} (options: sled, sqlite, lmdb)",
|
||||
e
|
||||
"Unsupported DB engine: {} (options: {})",
|
||||
e,
|
||||
vec![
|
||||
#[cfg(feature = "sled")]
|
||||
"sled",
|
||||
#[cfg(feature = "sqlite")]
|
||||
"sqlite",
|
||||
#[cfg(feature = "lmdb")]
|
||||
"lmdb",
|
||||
]
|
||||
.join(", ")
|
||||
)));
|
||||
}
|
||||
};
|
||||
@@ -133,7 +169,7 @@ impl Garage {
|
||||
background.clone(),
|
||||
replication_mode.replication_factor(),
|
||||
&config,
|
||||
);
|
||||
)?;
|
||||
|
||||
let data_rep_param = TableShardedReplication {
|
||||
system: system.clone(),
|
||||
@@ -159,7 +195,6 @@ impl Garage {
|
||||
&db,
|
||||
config.data_dir.clone(),
|
||||
config.compression_level,
|
||||
config.block_manager_background_tranquility,
|
||||
data_rep_param,
|
||||
system.clone(),
|
||||
);
|
||||
|
||||
@@ -81,7 +81,7 @@ impl<T: CountedItem> CounterEntry<T> {
|
||||
}
|
||||
|
||||
/// A counter entry in the global table
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct CounterValue {
|
||||
pub node_values: BTreeMap<Uuid, (u64, i64)>,
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ pub fn make_node_id(node_id: Uuid) -> K2VNodeId {
|
||||
u64::from_be_bytes(tmp)
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
pub struct CausalContext {
|
||||
pub vector_clock: BTreeMap<K2VNodeId, u64>,
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ pub const CONFLICTS: &str = "conflicts";
|
||||
pub const VALUES: &str = "values";
|
||||
pub const BYTES: &str = "bytes";
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct K2VItem {
|
||||
pub partition: K2VItemPartition,
|
||||
pub sort_key: String,
|
||||
@@ -25,19 +25,19 @@ pub struct K2VItem {
|
||||
items: BTreeMap<K2VNodeId, DvvsEntry>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash, Eq)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, Hash)]
|
||||
pub struct K2VItemPartition {
|
||||
pub bucket_id: Uuid,
|
||||
pub partition_key: String,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
struct DvvsEntry {
|
||||
t_discard: u64,
|
||||
values: Vec<(u64, DvvsValue)>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum DvvsValue {
|
||||
Value(#[serde(with = "serde_bytes")] Vec<u8>),
|
||||
Deleted,
|
||||
|
||||
+17
-19
@@ -23,7 +23,6 @@ use garage_rpc::system::System;
|
||||
use garage_rpc::*;
|
||||
|
||||
use garage_table::replication::{TableReplication, TableShardedReplication};
|
||||
use garage_table::table::TABLE_RPC_TIMEOUT;
|
||||
use garage_table::{PartitionKey, Table};
|
||||
|
||||
use crate::k2v::causality::*;
|
||||
@@ -117,7 +116,6 @@ impl K2VRpcHandler {
|
||||
}),
|
||||
RequestStrategy::with_priority(PRIO_NORMAL)
|
||||
.with_quorum(1)
|
||||
.with_timeout(TABLE_RPC_TIMEOUT)
|
||||
.interrupt_after_quorum(true),
|
||||
)
|
||||
.await?;
|
||||
@@ -169,7 +167,6 @@ impl K2VRpcHandler {
|
||||
K2VRpc::InsertManyItems(items),
|
||||
RequestStrategy::with_priority(PRIO_NORMAL)
|
||||
.with_quorum(1)
|
||||
.with_timeout(TABLE_RPC_TIMEOUT)
|
||||
.interrupt_after_quorum(true),
|
||||
)
|
||||
.await?;
|
||||
@@ -205,22 +202,23 @@ impl K2VRpcHandler {
|
||||
.replication
|
||||
.write_nodes(&poll_key.partition.hash());
|
||||
|
||||
let resps = self
|
||||
.system
|
||||
.rpc
|
||||
.try_call_many(
|
||||
&self.endpoint,
|
||||
&nodes[..],
|
||||
K2VRpc::PollItem {
|
||||
key: poll_key,
|
||||
causal_context,
|
||||
timeout_msec,
|
||||
},
|
||||
RequestStrategy::with_priority(PRIO_NORMAL)
|
||||
.with_quorum(self.item_table.data.replication.read_quorum())
|
||||
.with_timeout(Duration::from_millis(timeout_msec) + TABLE_RPC_TIMEOUT),
|
||||
)
|
||||
.await?;
|
||||
let rpc = self.system.rpc.try_call_many(
|
||||
&self.endpoint,
|
||||
&nodes[..],
|
||||
K2VRpc::PollItem {
|
||||
key: poll_key,
|
||||
causal_context,
|
||||
timeout_msec,
|
||||
},
|
||||
RequestStrategy::with_priority(PRIO_NORMAL)
|
||||
.with_quorum(self.item_table.data.replication.read_quorum())
|
||||
.without_timeout(),
|
||||
);
|
||||
let timeout_duration = Duration::from_millis(timeout_msec) + self.system.rpc.rpc_timeout();
|
||||
let resps = select! {
|
||||
r = rpc => r?,
|
||||
_ = tokio::time::sleep(timeout_duration) => return Ok(None),
|
||||
};
|
||||
|
||||
let mut resp: Option<K2VItem> = None;
|
||||
for v in resps {
|
||||
|
||||
@@ -6,10 +6,10 @@ use garage_util::data::*;
|
||||
|
||||
use crate::permission::BucketKeyPerm;
|
||||
|
||||
use garage_model_050::key_table as old;
|
||||
use crate::prev::v051::key_table as old;
|
||||
|
||||
/// An api key
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Key {
|
||||
/// The id of the key (immutable), used as partition key
|
||||
pub key_id: String,
|
||||
@@ -19,7 +19,7 @@ pub struct Key {
|
||||
}
|
||||
|
||||
/// Configuration for a key
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct KeyParams {
|
||||
/// The secret_key associated (immutable)
|
||||
pub secret_key: String,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
// For migration from previous versions
|
||||
pub(crate) mod prev;
|
||||
|
||||
pub mod permission;
|
||||
|
||||
pub mod index_counter;
|
||||
|
||||
@@ -5,7 +5,7 @@ use garage_util::data::*;
|
||||
use garage_util::error::Error as GarageError;
|
||||
use garage_util::time::*;
|
||||
|
||||
use garage_model_050::bucket_table as old_bucket;
|
||||
use crate::prev::v051::bucket_table as old_bucket;
|
||||
|
||||
use crate::bucket_alias_table::*;
|
||||
use crate::bucket_table::*;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub(crate) mod v051;
|
||||
@@ -0,0 +1,63 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use garage_table::crdt::Crdt;
|
||||
use garage_table::*;
|
||||
|
||||
use super::key_table::PermissionSet;
|
||||
|
||||
/// A bucket is a collection of objects
|
||||
///
|
||||
/// Its parameters are not directly accessible as:
|
||||
/// - It must be possible to merge paramaters, hence the use of a LWW CRDT.
|
||||
/// - A bucket has 2 states, Present or Deleted and parameters make sense only if present.
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Bucket {
|
||||
/// Name of the bucket
|
||||
pub name: String,
|
||||
/// State, and configuration if not deleted, of the bucket
|
||||
pub state: crdt::Lww<BucketState>,
|
||||
}
|
||||
|
||||
/// State of a bucket
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum BucketState {
|
||||
/// The bucket is deleted
|
||||
Deleted,
|
||||
/// The bucket exists
|
||||
Present(BucketParams),
|
||||
}
|
||||
|
||||
impl Crdt for BucketState {
|
||||
fn merge(&mut self, o: &Self) {
|
||||
match o {
|
||||
BucketState::Deleted => *self = BucketState::Deleted,
|
||||
BucketState::Present(other_params) => {
|
||||
if let BucketState::Present(params) = self {
|
||||
params.merge(other_params);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for a bucket
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct BucketParams {
|
||||
/// Map of key with access to the bucket, and what kind of access they give
|
||||
pub authorized_keys: crdt::LwwMap<String, PermissionSet>,
|
||||
/// Is the bucket served as http
|
||||
pub website: crdt::Lww<bool>,
|
||||
}
|
||||
|
||||
impl Crdt for BucketParams {
|
||||
fn merge(&mut self, o: &Self) {
|
||||
self.authorized_keys.merge(&o.authorized_keys);
|
||||
self.website.merge(&o.website);
|
||||
}
|
||||
}
|
||||
|
||||
impl Crdt for Bucket {
|
||||
fn merge(&mut self, other: &Self) {
|
||||
self.state.merge(&other.state);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use garage_table::crdt::*;
|
||||
use garage_table::*;
|
||||
|
||||
/// An api key
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Key {
|
||||
/// The id of the key (immutable), used as partition key
|
||||
pub key_id: String,
|
||||
|
||||
/// The secret_key associated
|
||||
pub secret_key: String,
|
||||
|
||||
/// Name for the key
|
||||
pub name: crdt::Lww<String>,
|
||||
|
||||
/// Is the key deleted
|
||||
pub deleted: crdt::Bool,
|
||||
|
||||
/// Buckets in which the key is authorized. Empty if `Key` is deleted
|
||||
// CRDT interaction: deleted implies authorized_buckets is empty
|
||||
pub authorized_buckets: crdt::LwwMap<String, PermissionSet>,
|
||||
}
|
||||
|
||||
/// Permission given to a key in a bucket
|
||||
#[derive(PartialOrd, Ord, PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PermissionSet {
|
||||
/// The key can be used to read the bucket
|
||||
pub allow_read: bool,
|
||||
/// The key can be used to write in the bucket
|
||||
pub allow_write: bool,
|
||||
}
|
||||
|
||||
impl AutoCrdt for PermissionSet {
|
||||
const WARN_IF_DIFFERENT: bool = true;
|
||||
}
|
||||
|
||||
impl Crdt for Key {
|
||||
fn merge(&mut self, other: &Self) {
|
||||
self.name.merge(&other.name);
|
||||
self.deleted.merge(&other.deleted);
|
||||
|
||||
if self.deleted.get() {
|
||||
self.authorized_buckets.clear();
|
||||
} else {
|
||||
self.authorized_buckets.merge(&other.authorized_buckets);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub(crate) mod bucket_table;
|
||||
pub(crate) mod key_table;
|
||||
pub(crate) mod object_table;
|
||||
pub(crate) mod version_table;
|
||||
@@ -0,0 +1,149 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use garage_util::data::*;
|
||||
|
||||
use garage_table::crdt::*;
|
||||
|
||||
/// An object
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Object {
|
||||
/// The bucket in which the object is stored, used as partition key
|
||||
pub bucket: String,
|
||||
|
||||
/// The key at which the object is stored in its bucket, used as sorting key
|
||||
pub key: String,
|
||||
|
||||
/// The list of currenty stored versions of the object
|
||||
versions: Vec<ObjectVersion>,
|
||||
}
|
||||
|
||||
impl Object {
|
||||
/// Get a list of currently stored versions of `Object`
|
||||
pub fn versions(&self) -> &[ObjectVersion] {
|
||||
&self.versions[..]
|
||||
}
|
||||
}
|
||||
|
||||
/// Informations about a version of an object
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ObjectVersion {
|
||||
/// Id of the version
|
||||
pub uuid: Uuid,
|
||||
/// Timestamp of when the object was created
|
||||
pub timestamp: u64,
|
||||
/// State of the version
|
||||
pub state: ObjectVersionState,
|
||||
}
|
||||
|
||||
/// State of an object version
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum ObjectVersionState {
|
||||
/// The version is being received
|
||||
Uploading(ObjectVersionHeaders),
|
||||
/// The version is fully received
|
||||
Complete(ObjectVersionData),
|
||||
/// The version uploaded containded errors or the upload was explicitly aborted
|
||||
Aborted,
|
||||
}
|
||||
|
||||
impl Crdt for ObjectVersionState {
|
||||
fn merge(&mut self, other: &Self) {
|
||||
use ObjectVersionState::*;
|
||||
match other {
|
||||
Aborted => {
|
||||
*self = Aborted;
|
||||
}
|
||||
Complete(b) => match self {
|
||||
Aborted => {}
|
||||
Complete(a) => {
|
||||
a.merge(b);
|
||||
}
|
||||
Uploading(_) => {
|
||||
*self = Complete(b.clone());
|
||||
}
|
||||
},
|
||||
Uploading(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Data stored in object version
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum ObjectVersionData {
|
||||
/// The object was deleted, this Version is a tombstone to mark it as such
|
||||
DeleteMarker,
|
||||
/// The object is short, it's stored inlined
|
||||
Inline(ObjectVersionMeta, #[serde(with = "serde_bytes")] Vec<u8>),
|
||||
/// The object is not short, Hash of first block is stored here, next segments hashes are
|
||||
/// stored in the version table
|
||||
FirstBlock(ObjectVersionMeta, Hash),
|
||||
}
|
||||
|
||||
impl AutoCrdt for ObjectVersionData {
|
||||
const WARN_IF_DIFFERENT: bool = true;
|
||||
}
|
||||
|
||||
/// Metadata about the object version
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ObjectVersionMeta {
|
||||
/// Headers to send to the client
|
||||
pub headers: ObjectVersionHeaders,
|
||||
/// Size of the object
|
||||
pub size: u64,
|
||||
/// etag of the object
|
||||
pub etag: String,
|
||||
}
|
||||
|
||||
/// Additional headers for an object
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ObjectVersionHeaders {
|
||||
/// Content type of the object
|
||||
pub content_type: String,
|
||||
/// Any other http headers to send
|
||||
pub other: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl ObjectVersion {
|
||||
fn cmp_key(&self) -> (u64, Uuid) {
|
||||
(self.timestamp, self.uuid)
|
||||
}
|
||||
|
||||
/// Is the object version completely received
|
||||
pub fn is_complete(&self) -> bool {
|
||||
matches!(self.state, ObjectVersionState::Complete(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl Crdt for Object {
|
||||
fn merge(&mut self, other: &Self) {
|
||||
// Merge versions from other into here
|
||||
for other_v in other.versions.iter() {
|
||||
match self
|
||||
.versions
|
||||
.binary_search_by(|v| v.cmp_key().cmp(&other_v.cmp_key()))
|
||||
{
|
||||
Ok(i) => {
|
||||
self.versions[i].state.merge(&other_v.state);
|
||||
}
|
||||
Err(i) => {
|
||||
self.versions.insert(i, other_v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove versions which are obsolete, i.e. those that come
|
||||
// before the last version which .is_complete().
|
||||
let last_complete = self
|
||||
.versions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.find(|(_, v)| v.is_complete())
|
||||
.map(|(vi, _)| vi);
|
||||
|
||||
if let Some(last_vi) = last_complete {
|
||||
self.versions = self.versions.drain(last_vi..).collect::<Vec<_>>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use garage_util::data::*;
|
||||
|
||||
use garage_table::crdt::*;
|
||||
use garage_table::*;
|
||||
|
||||
/// A version of an object
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Version {
|
||||
/// UUID of the version, used as partition key
|
||||
pub uuid: Uuid,
|
||||
|
||||
// Actual data: the blocks for this version
|
||||
// In the case of a multipart upload, also store the etags
|
||||
// of individual parts and check them when doing CompleteMultipartUpload
|
||||
/// Is this version deleted
|
||||
pub deleted: crdt::Bool,
|
||||
/// list of blocks of data composing the version
|
||||
pub blocks: crdt::Map<VersionBlockKey, VersionBlock>,
|
||||
/// Etag of each part in case of a multipart upload, empty otherwise
|
||||
pub parts_etags: crdt::Map<u64, String>,
|
||||
|
||||
// Back link to bucket+key so that we can figure if
|
||||
// this was deleted later on
|
||||
/// Bucket in which the related object is stored
|
||||
pub bucket: String,
|
||||
/// Key in which the related object is stored
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub struct VersionBlockKey {
|
||||
/// Number of the part
|
||||
pub part_number: u64,
|
||||
/// Offset of this sub-segment in its part
|
||||
pub offset: u64,
|
||||
}
|
||||
|
||||
impl Ord for VersionBlockKey {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.part_number
|
||||
.cmp(&other.part_number)
|
||||
.then(self.offset.cmp(&other.offset))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for VersionBlockKey {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
/// Informations about a single block
|
||||
#[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub struct VersionBlock {
|
||||
/// Blake2 sum of the block
|
||||
pub hash: Hash,
|
||||
/// Size of the block
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
impl AutoCrdt for VersionBlock {
|
||||
const WARN_IF_DIFFERENT: bool = true;
|
||||
}
|
||||
|
||||
impl Crdt for Version {
|
||||
fn merge(&mut self, other: &Self) {
|
||||
self.deleted.merge(&other.deleted);
|
||||
|
||||
if self.deleted.get() {
|
||||
self.blocks.clear();
|
||||
self.parts_etags.clear();
|
||||
} else {
|
||||
self.blocks.merge(&other.blocks);
|
||||
self.parts_etags.merge(&other.parts_etags);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ use garage_table::*;
|
||||
|
||||
use garage_block::manager::*;
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct BlockRef {
|
||||
/// Hash (blake2 sum) of the block, used as partition key
|
||||
pub block: Hash,
|
||||
|
||||
@@ -14,14 +14,14 @@ use garage_table::*;
|
||||
use crate::index_counter::*;
|
||||
use crate::s3::version_table::*;
|
||||
|
||||
use garage_model_050::object_table as old;
|
||||
use crate::prev::v051::object_table as old;
|
||||
|
||||
pub const OBJECTS: &str = "objects";
|
||||
pub const UNFINISHED_UPLOADS: &str = "unfinished_uploads";
|
||||
pub const BYTES: &str = "bytes";
|
||||
|
||||
/// An object
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Object {
|
||||
/// The bucket in which the object is stored, used as partition key
|
||||
pub bucket_id: Uuid,
|
||||
@@ -70,7 +70,7 @@ impl Object {
|
||||
}
|
||||
|
||||
/// Informations about a version of an object
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ObjectVersion {
|
||||
/// Id of the version
|
||||
pub uuid: Uuid,
|
||||
@@ -81,7 +81,7 @@ pub struct ObjectVersion {
|
||||
}
|
||||
|
||||
/// State of an object version
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum ObjectVersionState {
|
||||
/// The version is being received
|
||||
Uploading(ObjectVersionHeaders),
|
||||
|
||||
@@ -12,10 +12,10 @@ use garage_table::*;
|
||||
|
||||
use crate::s3::block_ref_table::*;
|
||||
|
||||
use garage_model_050::version_table as old;
|
||||
use crate::prev::v051::version_table as old;
|
||||
|
||||
/// A version of an object
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Version {
|
||||
/// UUID of the version, used as partition key
|
||||
pub uuid: Uuid,
|
||||
|
||||
+4
-6
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_rpc"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
@@ -14,12 +14,11 @@ path = "lib.rs"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
garage_util = { version = "0.7.0", path = "../util" }
|
||||
garage_util = { version = "0.8.0", path = "../util" }
|
||||
|
||||
arc-swap = "1.0"
|
||||
bytes = "1.0"
|
||||
gethostname = "0.2"
|
||||
git-version = "0.3.4"
|
||||
hex = "0.4"
|
||||
tracing = "0.1.30"
|
||||
rand = "0.8"
|
||||
@@ -46,12 +45,11 @@ tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi
|
||||
tokio-stream = { version = "0.1", features = ["net"] }
|
||||
opentelemetry = "0.17"
|
||||
|
||||
#netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
|
||||
#netapp = { version = "0.4", path = "../../../netapp", features = ["telemetry"] }
|
||||
netapp = { version = "0.4.4", features = ["telemetry"] }
|
||||
netapp = { version = "0.5.2", features = ["telemetry"] }
|
||||
|
||||
hyper = { version = "0.14", features = ["client", "http1", "runtime", "tcp"] }
|
||||
|
||||
|
||||
[features]
|
||||
kubernetes-discovery = [ "kube", "k8s-openapi", "openssl", "schemars" ]
|
||||
system-libs = [ "sodiumoxide/use-pkg-config" ]
|
||||
|
||||
@@ -56,7 +56,7 @@ pub async fn get_kubernetes_nodes(
|
||||
let mut ret = Vec::with_capacity(nodes.items.len());
|
||||
|
||||
for node in nodes {
|
||||
println!("Found Pod: {:?}", node.metadata.name);
|
||||
info!("Found Pod: {:?}", node.metadata.name);
|
||||
|
||||
let pubkey = &node
|
||||
.metadata
|
||||
|
||||
+1
-18
@@ -1,31 +1,18 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use opentelemetry::{global, metrics::*};
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
/// TableMetrics reference all counter used for metrics
|
||||
pub struct RpcMetrics {
|
||||
pub(crate) _rpc_available_permits: ValueObserver<u64>,
|
||||
|
||||
pub(crate) rpc_counter: Counter<u64>,
|
||||
pub(crate) rpc_timeout_counter: Counter<u64>,
|
||||
pub(crate) rpc_netapp_error_counter: Counter<u64>,
|
||||
pub(crate) rpc_garage_error_counter: Counter<u64>,
|
||||
|
||||
pub(crate) rpc_duration: ValueRecorder<f64>,
|
||||
pub(crate) rpc_queueing_time: ValueRecorder<f64>,
|
||||
}
|
||||
impl RpcMetrics {
|
||||
pub fn new(sem: Arc<Semaphore>) -> Self {
|
||||
pub fn new() -> Self {
|
||||
let meter = global::meter("garage_rpc");
|
||||
RpcMetrics {
|
||||
_rpc_available_permits: meter
|
||||
.u64_value_observer("rpc.available_permits", move |observer| {
|
||||
observer.observe(sem.available_permits() as u64, &[])
|
||||
})
|
||||
.with_description("Number of available RPC permits")
|
||||
.init(),
|
||||
|
||||
rpc_counter: meter
|
||||
.u64_counter("rpc.request_counter")
|
||||
.with_description("Number of RPC requests emitted")
|
||||
@@ -46,10 +33,6 @@ impl RpcMetrics {
|
||||
.f64_value_recorder("rpc.duration")
|
||||
.with_description("Duration of RPCs")
|
||||
.init(),
|
||||
rpc_queueing_time: meter
|
||||
.f64_value_recorder("rpc.queueing_time")
|
||||
.with_description("Time RPC requests were queued for before being sent")
|
||||
.init(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+133
-111
@@ -7,7 +7,7 @@ use futures::stream::futures_unordered::FuturesUnordered;
|
||||
use futures::stream::StreamExt;
|
||||
use futures_util::future::FutureExt;
|
||||
use tokio::select;
|
||||
use tokio::sync::{watch, Semaphore};
|
||||
use tokio::sync::watch;
|
||||
|
||||
use opentelemetry::KeyValue;
|
||||
use opentelemetry::{
|
||||
@@ -15,10 +15,14 @@ use opentelemetry::{
|
||||
Context,
|
||||
};
|
||||
|
||||
pub use netapp::endpoint::{Endpoint, EndpointHandler, Message as Rpc};
|
||||
pub use netapp::endpoint::{Endpoint, EndpointHandler, StreamingEndpointHandler};
|
||||
use netapp::message::IntoReq;
|
||||
pub use netapp::message::{
|
||||
Message as Rpc, OrderTag, Req, RequestPriority, Resp, PRIO_BACKGROUND, PRIO_HIGH, PRIO_NORMAL,
|
||||
PRIO_SECONDARY,
|
||||
};
|
||||
use netapp::peering::fullmesh::FullMeshPeeringStrategy;
|
||||
pub use netapp::proto::*;
|
||||
pub use netapp::{NetApp, NodeID};
|
||||
pub use netapp::{self, NetApp, NodeID};
|
||||
|
||||
use garage_util::background::BackgroundRunner;
|
||||
use garage_util::data::*;
|
||||
@@ -28,34 +32,37 @@ use garage_util::metrics::RecordDuration;
|
||||
use crate::metrics::RpcMetrics;
|
||||
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;
|
||||
// Default RPC timeout = 5 minutes
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(300);
|
||||
|
||||
/// Strategy to apply when making RPC
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct RequestStrategy {
|
||||
/// Max time to wait for reponse
|
||||
pub rs_timeout: Duration,
|
||||
/// Min number of response to consider the request successful
|
||||
pub rs_quorum: Option<usize>,
|
||||
/// Should requests be dropped after enough response are received
|
||||
pub rs_interrupt_after_quorum: bool,
|
||||
/// Request priority
|
||||
pub rs_priority: RequestPriority,
|
||||
/// Custom timeout for this request
|
||||
rs_timeout: Timeout,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum Timeout {
|
||||
None,
|
||||
Default,
|
||||
Custom(Duration),
|
||||
}
|
||||
|
||||
impl RequestStrategy {
|
||||
/// Create a RequestStrategy with default timeout and not interrupting when quorum reached
|
||||
pub fn with_priority(prio: RequestPriority) -> Self {
|
||||
RequestStrategy {
|
||||
rs_timeout: DEFAULT_TIMEOUT,
|
||||
rs_quorum: None,
|
||||
rs_interrupt_after_quorum: false,
|
||||
rs_priority: prio,
|
||||
rs_timeout: Timeout::Default,
|
||||
}
|
||||
}
|
||||
/// Set quorum to be reached for request
|
||||
@@ -63,17 +70,22 @@ impl RequestStrategy {
|
||||
self.rs_quorum = Some(quorum);
|
||||
self
|
||||
}
|
||||
/// Set timeout of the strategy
|
||||
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.rs_timeout = timeout;
|
||||
self
|
||||
}
|
||||
/// Set if requests can be dropped after quorum has been reached
|
||||
/// In general true for read requests, and false for write
|
||||
pub fn interrupt_after_quorum(mut self, interrupt: bool) -> Self {
|
||||
self.rs_interrupt_after_quorum = interrupt;
|
||||
self
|
||||
}
|
||||
/// Deactivate timeout for this request
|
||||
pub fn without_timeout(mut self) -> Self {
|
||||
self.rs_timeout = Timeout::None;
|
||||
self
|
||||
}
|
||||
/// Set custom timeout for this request
|
||||
pub fn with_custom_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.rs_timeout = Timeout::Custom(timeout);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -84,8 +96,8 @@ struct RpcHelperInner {
|
||||
fullmesh: Arc<FullMeshPeeringStrategy>,
|
||||
background: Arc<BackgroundRunner>,
|
||||
ring: watch::Receiver<Arc<Ring>>,
|
||||
request_buffer_semaphore: Arc<Semaphore>,
|
||||
metrics: RpcMetrics,
|
||||
rpc_timeout: Duration,
|
||||
}
|
||||
|
||||
impl RpcHelper {
|
||||
@@ -94,45 +106,35 @@ impl RpcHelper {
|
||||
fullmesh: Arc<FullMeshPeeringStrategy>,
|
||||
background: Arc<BackgroundRunner>,
|
||||
ring: watch::Receiver<Arc<Ring>>,
|
||||
rpc_timeout: Option<Duration>,
|
||||
) -> Self {
|
||||
let sem = Arc::new(Semaphore::new(REQUEST_BUFFER_SIZE));
|
||||
|
||||
let metrics = RpcMetrics::new(sem.clone());
|
||||
let metrics = RpcMetrics::new();
|
||||
|
||||
Self(Arc::new(RpcHelperInner {
|
||||
our_node_id,
|
||||
fullmesh,
|
||||
background,
|
||||
ring,
|
||||
request_buffer_semaphore: sem,
|
||||
metrics,
|
||||
rpc_timeout: rpc_timeout.unwrap_or(DEFAULT_TIMEOUT),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn call<M, H, S>(
|
||||
&self,
|
||||
endpoint: &Endpoint<M, H>,
|
||||
to: Uuid,
|
||||
msg: M,
|
||||
strat: RequestStrategy,
|
||||
) -> Result<S, Error>
|
||||
where
|
||||
M: Rpc<Response = Result<S, Error>>,
|
||||
H: EndpointHandler<M>,
|
||||
{
|
||||
self.call_arc(endpoint, to, Arc::new(msg), strat).await
|
||||
pub fn rpc_timeout(&self) -> Duration {
|
||||
self.0.rpc_timeout
|
||||
}
|
||||
|
||||
pub async fn call_arc<M, H, S>(
|
||||
pub async fn call<M, N, H, S>(
|
||||
&self,
|
||||
endpoint: &Endpoint<M, H>,
|
||||
to: Uuid,
|
||||
msg: Arc<M>,
|
||||
msg: N,
|
||||
strat: RequestStrategy,
|
||||
) -> Result<S, Error>
|
||||
where
|
||||
M: Rpc<Response = Result<S, Error>>,
|
||||
H: EndpointHandler<M>,
|
||||
N: IntoReq<M> + Send,
|
||||
H: StreamingEndpointHandler<M>,
|
||||
{
|
||||
let metric_tags = [
|
||||
KeyValue::new("rpc_endpoint", endpoint.path().to_string()),
|
||||
@@ -140,29 +142,27 @@ impl RpcHelper {
|
||||
KeyValue::new("to", format!("{:?}", to)),
|
||||
];
|
||||
|
||||
let msg_size = rmp_to_vec_all_named(&msg)?.len() as u32;
|
||||
let permit = self
|
||||
.0
|
||||
.request_buffer_semaphore
|
||||
.acquire_many(msg_size)
|
||||
.record_duration(&self.0.metrics.rpc_queueing_time, &metric_tags)
|
||||
.await?;
|
||||
|
||||
self.0.metrics.rpc_counter.add(1, &metric_tags);
|
||||
|
||||
let node_id = to.into();
|
||||
let rpc_call = endpoint
|
||||
.call(&node_id, msg, strat.rs_priority)
|
||||
.call_streaming(&node_id, msg, strat.rs_priority)
|
||||
.record_duration(&self.0.metrics.rpc_duration, &metric_tags);
|
||||
|
||||
let timeout = async {
|
||||
match strat.rs_timeout {
|
||||
Timeout::None => futures::future::pending().await,
|
||||
Timeout::Default => tokio::time::sleep(self.0.rpc_timeout).await,
|
||||
Timeout::Custom(t) => tokio::time::sleep(t).await,
|
||||
}
|
||||
};
|
||||
|
||||
select! {
|
||||
res = rpc_call => {
|
||||
drop(permit);
|
||||
|
||||
if res.is_err() {
|
||||
self.0.metrics.rpc_netapp_error_counter.add(1, &metric_tags);
|
||||
}
|
||||
let res = res?;
|
||||
let res = res?.into_msg();
|
||||
|
||||
if res.is_err() {
|
||||
self.0.metrics.rpc_garage_error_counter.add(1, &metric_tags);
|
||||
@@ -170,46 +170,49 @@ impl RpcHelper {
|
||||
|
||||
Ok(res?)
|
||||
}
|
||||
_ = tokio::time::sleep(strat.rs_timeout) => {
|
||||
drop(permit);
|
||||
() = timeout => {
|
||||
self.0.metrics.rpc_timeout_counter.add(1, &metric_tags);
|
||||
Err(Error::Timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn call_many<M, H, S>(
|
||||
pub async fn call_many<M, N, H, S>(
|
||||
&self,
|
||||
endpoint: &Endpoint<M, H>,
|
||||
to: &[Uuid],
|
||||
msg: M,
|
||||
msg: N,
|
||||
strat: RequestStrategy,
|
||||
) -> Vec<(Uuid, Result<S, Error>)>
|
||||
) -> Result<Vec<(Uuid, Result<S, Error>)>, Error>
|
||||
where
|
||||
M: Rpc<Response = Result<S, Error>>,
|
||||
H: EndpointHandler<M>,
|
||||
N: IntoReq<M>,
|
||||
H: StreamingEndpointHandler<M>,
|
||||
{
|
||||
let msg = Arc::new(msg);
|
||||
let msg = msg.into_req().map_err(netapp::error::Error::from)?;
|
||||
|
||||
let resps = join_all(
|
||||
to.iter()
|
||||
.map(|to| self.call_arc(endpoint, *to, msg.clone(), strat)),
|
||||
.map(|to| self.call(endpoint, *to, msg.clone(), strat)),
|
||||
)
|
||||
.await;
|
||||
to.iter()
|
||||
Ok(to
|
||||
.iter()
|
||||
.cloned()
|
||||
.zip(resps.into_iter())
|
||||
.collect::<Vec<_>>()
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
pub async fn broadcast<M, H, S>(
|
||||
pub async fn broadcast<M, N, H, S>(
|
||||
&self,
|
||||
endpoint: &Endpoint<M, H>,
|
||||
msg: M,
|
||||
msg: N,
|
||||
strat: RequestStrategy,
|
||||
) -> Vec<(Uuid, Result<S, Error>)>
|
||||
) -> Result<Vec<(Uuid, Result<S, Error>)>, Error>
|
||||
where
|
||||
M: Rpc<Response = Result<S, Error>>,
|
||||
H: EndpointHandler<M>,
|
||||
N: IntoReq<M>,
|
||||
H: StreamingEndpointHandler<M>,
|
||||
{
|
||||
let to = self
|
||||
.0
|
||||
@@ -223,16 +226,17 @@ impl RpcHelper {
|
||||
|
||||
/// Make a RPC call to multiple servers, returning either a Vec of responses,
|
||||
/// or an error if quorum could not be reached due to too many errors
|
||||
pub async fn try_call_many<M, H, S>(
|
||||
pub async fn try_call_many<M, N, H, S>(
|
||||
&self,
|
||||
endpoint: &Arc<Endpoint<M, H>>,
|
||||
to: &[Uuid],
|
||||
msg: M,
|
||||
msg: N,
|
||||
strategy: RequestStrategy,
|
||||
) -> Result<Vec<S>, Error>
|
||||
where
|
||||
M: Rpc<Response = Result<S, Error>> + 'static,
|
||||
H: EndpointHandler<M> + 'static,
|
||||
N: IntoReq<M>,
|
||||
H: StreamingEndpointHandler<M> + 'static,
|
||||
S: Send + 'static,
|
||||
{
|
||||
let quorum = strategy.rs_quorum.unwrap_or(to.len());
|
||||
@@ -262,20 +266,21 @@ impl RpcHelper {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn try_call_many_internal<M, H, S>(
|
||||
async fn try_call_many_internal<M, N, H, S>(
|
||||
&self,
|
||||
endpoint: &Arc<Endpoint<M, H>>,
|
||||
to: &[Uuid],
|
||||
msg: M,
|
||||
msg: N,
|
||||
strategy: RequestStrategy,
|
||||
quorum: usize,
|
||||
) -> Result<Vec<S>, Error>
|
||||
where
|
||||
M: Rpc<Response = Result<S, Error>> + 'static,
|
||||
H: EndpointHandler<M> + 'static,
|
||||
N: IntoReq<M>,
|
||||
H: StreamingEndpointHandler<M> + 'static,
|
||||
S: Send + 'static,
|
||||
{
|
||||
let msg = Arc::new(msg);
|
||||
let msg = msg.into_req().map_err(netapp::error::Error::from)?;
|
||||
|
||||
// Build future for each request
|
||||
// They are not started now: they are added below in a FuturesUnordered
|
||||
@@ -285,7 +290,7 @@ impl RpcHelper {
|
||||
let msg = msg.clone();
|
||||
let endpoint2 = endpoint.clone();
|
||||
(to, async move {
|
||||
self2.call_arc(&endpoint2, to, msg, strategy).await
|
||||
self2.call(&endpoint2, to, msg, strategy).await
|
||||
})
|
||||
});
|
||||
|
||||
@@ -299,47 +304,19 @@ impl RpcHelper {
|
||||
// to reach a quorum, priorizing nodes with the lowest latency.
|
||||
// When there are errors, we start new requests to compensate.
|
||||
|
||||
// Retrieve some status variables that we will use to sort requests
|
||||
let peer_list = self.0.fullmesh.get_peer_list();
|
||||
let ring: Arc<Ring> = self.0.ring.borrow().clone();
|
||||
let our_zone = match ring.layout.node_role(&self.0.our_node_id) {
|
||||
Some(pc) => &pc.zone,
|
||||
None => "",
|
||||
};
|
||||
|
||||
// Augment requests with some information used to sort them.
|
||||
// The tuples are as follows:
|
||||
// (is another node?, is another zone?, latency, node ID, request future)
|
||||
// We store all of these tuples in a vec that we can sort.
|
||||
// By sorting this vec, we priorize ourself, then nodes in the same zone,
|
||||
// and within a same zone we priorize nodes with the lowest latency.
|
||||
let mut requests = requests
|
||||
.map(|(to, fut)| {
|
||||
let peer_zone = match ring.layout.node_role(&to) {
|
||||
Some(pc) => &pc.zone,
|
||||
None => "",
|
||||
};
|
||||
let peer_avg_ping = peer_list
|
||||
.iter()
|
||||
.find(|x| x.id.as_ref() == to.as_slice())
|
||||
.and_then(|pi| pi.avg_ping)
|
||||
.unwrap_or_else(|| Duration::from_secs(1));
|
||||
(
|
||||
to != self.0.our_node_id,
|
||||
peer_zone != our_zone,
|
||||
peer_avg_ping,
|
||||
to,
|
||||
fut,
|
||||
)
|
||||
})
|
||||
// Reorder requests to priorize closeness / low latency
|
||||
let request_order = self.request_order(to);
|
||||
let mut ord_requests = vec![(); request_order.len()]
|
||||
.into_iter()
|
||||
.map(|_| None)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sort requests by (priorize ourself, priorize same zone, priorize low latency)
|
||||
requests
|
||||
.sort_by_key(|(diffnode, diffzone, ping, _to, _fut)| (*diffnode, *diffzone, *ping));
|
||||
for (to, fut) in requests {
|
||||
let i = request_order.iter().position(|x| *x == to).unwrap();
|
||||
ord_requests[i] = Some((to, fut));
|
||||
}
|
||||
|
||||
// Make an iterator to take requests in their sorted order
|
||||
let mut requests = requests.into_iter();
|
||||
let mut requests = ord_requests.into_iter().map(Option::unwrap);
|
||||
|
||||
// resp_stream will contain all of the requests that are currently in flight.
|
||||
// (for the moment none, they will be added in the loop below)
|
||||
@@ -350,7 +327,7 @@ impl RpcHelper {
|
||||
// If the current set of requests that are running is not enough to possibly
|
||||
// reach quorum, start some new requests.
|
||||
while successes.len() + resp_stream.len() < quorum {
|
||||
if let Some((_, _, _, req_to, fut)) = requests.next() {
|
||||
if let Some((req_to, fut)) = requests.next() {
|
||||
let tracer = opentelemetry::global::tracer("garage");
|
||||
let span = tracer.start(format!("RPC to {:?}", req_to));
|
||||
resp_stream.push(tokio::spawn(
|
||||
@@ -420,4 +397,49 @@ impl RpcHelper {
|
||||
Err(Error::Quorum(quorum, successes.len(), to.len(), errors))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_order(&self, nodes: &[Uuid]) -> Vec<Uuid> {
|
||||
// Retrieve some status variables that we will use to sort requests
|
||||
let peer_list = self.0.fullmesh.get_peer_list();
|
||||
let ring: Arc<Ring> = self.0.ring.borrow().clone();
|
||||
let our_zone = match ring.layout.node_role(&self.0.our_node_id) {
|
||||
Some(pc) => &pc.zone,
|
||||
None => "",
|
||||
};
|
||||
|
||||
// Augment requests with some information used to sort them.
|
||||
// The tuples are as follows:
|
||||
// (is another node?, is another zone?, latency, node ID, request future)
|
||||
// We store all of these tuples in a vec that we can sort.
|
||||
// By sorting this vec, we priorize ourself, then nodes in the same zone,
|
||||
// and within a same zone we priorize nodes with the lowest latency.
|
||||
let mut nodes = nodes
|
||||
.iter()
|
||||
.map(|to| {
|
||||
let peer_zone = match ring.layout.node_role(to) {
|
||||
Some(pc) => &pc.zone,
|
||||
None => "",
|
||||
};
|
||||
let peer_avg_ping = peer_list
|
||||
.iter()
|
||||
.find(|x| x.id.as_ref() == to.as_slice())
|
||||
.and_then(|pi| pi.avg_ping)
|
||||
.unwrap_or_else(|| Duration::from_secs(10));
|
||||
(
|
||||
*to != self.0.our_node_id,
|
||||
peer_zone != our_zone,
|
||||
peer_avg_ping,
|
||||
*to,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sort requests by (priorize ourself, priorize same zone, priorize low latency)
|
||||
nodes.sort_by_key(|(diffnode, diffzone, ping, _to)| (*diffnode, *diffzone, *ping));
|
||||
|
||||
nodes
|
||||
.into_iter()
|
||||
.map(|(_, _, _, to)| to)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
+102
-38
@@ -16,9 +16,9 @@ use tokio::sync::watch;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use netapp::endpoint::{Endpoint, EndpointHandler};
|
||||
use netapp::message::*;
|
||||
use netapp::peering::fullmesh::FullMeshPeeringStrategy;
|
||||
use netapp::proto::*;
|
||||
use netapp::util::parse_and_resolve_peer_addr;
|
||||
use netapp::util::parse_and_resolve_peer_addr_async;
|
||||
use netapp::{NetApp, NetworkKey, NodeID, NodeKey};
|
||||
|
||||
use garage_util::background::BackgroundRunner;
|
||||
@@ -37,10 +37,11 @@ use crate::rpc_helper::*;
|
||||
|
||||
const DISCOVERY_INTERVAL: Duration = Duration::from_secs(60);
|
||||
const STATUS_EXCHANGE_INTERVAL: Duration = Duration::from_secs(10);
|
||||
const PING_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
|
||||
/// Version tag used for version check upon Netapp connection
|
||||
pub const GARAGE_VERSION_TAG: u64 = 0x6761726167650007; // garage 0x0007
|
||||
/// Version tag used for version check upon Netapp connection.
|
||||
/// Cluster nodes with different version tags are deemed
|
||||
/// incompatible and will refuse to connect.
|
||||
pub const GARAGE_VERSION_TAG: u64 = 0x6761726167650008; // garage 0x0008
|
||||
|
||||
/// RPC endpoint used for calls related to membership
|
||||
pub const SYSTEM_RPC_PATH: &str = "garage_rpc/membership.rs/SystemRpc";
|
||||
@@ -90,7 +91,7 @@ pub struct System {
|
||||
|
||||
rpc_listen_addr: SocketAddr,
|
||||
rpc_public_addr: Option<SocketAddr>,
|
||||
bootstrap_peers: Vec<(NodeID, SocketAddr)>,
|
||||
bootstrap_peers: Vec<String>,
|
||||
|
||||
consul_discovery: Option<ConsulDiscoveryParam>,
|
||||
#[cfg(feature = "kubernetes-discovery")]
|
||||
@@ -196,7 +197,7 @@ impl System {
|
||||
background: Arc<BackgroundRunner>,
|
||||
replication_factor: usize,
|
||||
config: &Config,
|
||||
) -> Arc<Self> {
|
||||
) -> Result<Arc<Self>, Error> {
|
||||
let node_key =
|
||||
gen_node_key(&config.metadata_dir).expect("Unable to read or generate node ID");
|
||||
info!(
|
||||
@@ -204,11 +205,21 @@ impl System {
|
||||
hex::encode(&node_key.public_key()[..8])
|
||||
);
|
||||
|
||||
let persist_cluster_layout = Persister::new(&config.metadata_dir, "cluster_layout");
|
||||
let persist_cluster_layout: Persister<ClusterLayout> =
|
||||
Persister::new(&config.metadata_dir, "cluster_layout");
|
||||
let persist_peer_list = Persister::new(&config.metadata_dir, "peer_list");
|
||||
|
||||
let cluster_layout = match persist_cluster_layout.load() {
|
||||
Ok(x) => x,
|
||||
Ok(x) => {
|
||||
if x.replication_factor != replication_factor {
|
||||
return Err(Error::Message(format!(
|
||||
"Prevous cluster layout has replication factor {}, which is different than the one specified in the config file ({}). The previous cluster layout can be purged, if you know what you are doing, simply by deleting the `cluster_layout` file in your metadata directory.",
|
||||
x.replication_factor,
|
||||
replication_factor
|
||||
)));
|
||||
}
|
||||
x
|
||||
}
|
||||
Err(e) => {
|
||||
info!(
|
||||
"No valid previous cluster layout stored ({}), starting fresh.",
|
||||
@@ -230,8 +241,29 @@ impl System {
|
||||
let ring = Ring::new(cluster_layout, replication_factor);
|
||||
let (update_ring, ring) = watch::channel(Arc::new(ring));
|
||||
|
||||
let rpc_public_addr = match config.rpc_public_addr {
|
||||
Some(a) => Some(a),
|
||||
let rpc_public_addr = match &config.rpc_public_addr {
|
||||
Some(a_str) => {
|
||||
use std::net::ToSocketAddrs;
|
||||
match a_str.to_socket_addrs() {
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Cannot resolve rpc_public_addr {} from config file: {}.",
|
||||
a_str, e
|
||||
);
|
||||
None
|
||||
}
|
||||
Ok(a) => {
|
||||
let a = a.collect::<Vec<_>>();
|
||||
if a.is_empty() {
|
||||
error!("rpc_public_addr {} resolve to no known IP address", a_str);
|
||||
}
|
||||
if a.len() > 1 {
|
||||
warn!("Multiple possible resolutions for rpc_public_addr: {:?}. Taking the first one.", a);
|
||||
}
|
||||
a.into_iter().next()
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let addr =
|
||||
get_default_ip().map(|ip| SocketAddr::new(ip, config.rpc_bind_addr.port()));
|
||||
@@ -241,13 +273,15 @@ impl System {
|
||||
addr
|
||||
}
|
||||
};
|
||||
if rpc_public_addr.is_none() {
|
||||
warn!("This Garage node does not know its publicly reachable RPC address, this might hamper intra-cluster communication.");
|
||||
}
|
||||
|
||||
let netapp = NetApp::new(GARAGE_VERSION_TAG, network_key, node_key);
|
||||
let fullmesh = FullMeshPeeringStrategy::new(
|
||||
netapp.clone(),
|
||||
config.bootstrap_peers.clone(),
|
||||
rpc_public_addr,
|
||||
);
|
||||
let fullmesh = FullMeshPeeringStrategy::new(netapp.clone(), vec![], rpc_public_addr);
|
||||
if let Some(ping_timeout) = config.rpc_ping_timeout_msec {
|
||||
fullmesh.set_ping_timeout_millis(ping_timeout);
|
||||
}
|
||||
|
||||
let system_endpoint = netapp.endpoint(SYSTEM_RPC_PATH.into());
|
||||
|
||||
@@ -285,7 +319,13 @@ impl System {
|
||||
node_status: RwLock::new(HashMap::new()),
|
||||
netapp: netapp.clone(),
|
||||
fullmesh: fullmesh.clone(),
|
||||
rpc: RpcHelper::new(netapp.id.into(), fullmesh, background.clone(), ring.clone()),
|
||||
rpc: RpcHelper::new(
|
||||
netapp.id.into(),
|
||||
fullmesh,
|
||||
background.clone(),
|
||||
ring.clone(),
|
||||
config.rpc_timeout_msec.map(Duration::from_millis),
|
||||
),
|
||||
system_endpoint,
|
||||
replication_factor,
|
||||
rpc_listen_addr: config.rpc_bind_addr,
|
||||
@@ -301,7 +341,7 @@ impl System {
|
||||
metadata_dir: config.metadata_dir.clone(),
|
||||
});
|
||||
sys.system_endpoint.set_handler(sys.clone());
|
||||
sys
|
||||
Ok(sys)
|
||||
}
|
||||
|
||||
/// Perform bootstraping, starting the ping loop
|
||||
@@ -319,14 +359,6 @@ impl System {
|
||||
// ---- Administrative operations (directly available and
|
||||
// also available through RPC) ----
|
||||
|
||||
pub fn garage_version(&self) -> &'static str {
|
||||
option_env!("GIT_VERSION").unwrap_or(git_version::git_version!(
|
||||
prefix = "git:",
|
||||
cargo_prefix = "cargo:",
|
||||
fallback = "unknown"
|
||||
))
|
||||
}
|
||||
|
||||
pub fn get_known_nodes(&self) -> Vec<KnownNodeInfo> {
|
||||
let node_status = self.node_status.read().unwrap();
|
||||
let known_nodes = self
|
||||
@@ -366,12 +398,14 @@ impl System {
|
||||
}
|
||||
|
||||
pub async fn connect(&self, node: &str) -> Result<(), Error> {
|
||||
let (pubkey, addrs) = parse_and_resolve_peer_addr(node).ok_or_else(|| {
|
||||
Error::Message(format!(
|
||||
"Unable to parse or resolve node specification: {}",
|
||||
node
|
||||
))
|
||||
})?;
|
||||
let (pubkey, addrs) = parse_and_resolve_peer_addr_async(node)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
Error::Message(format!(
|
||||
"Unable to parse or resolve node specification: {}",
|
||||
node
|
||||
))
|
||||
})?;
|
||||
let mut errors = vec![];
|
||||
for ip in addrs.iter() {
|
||||
match self
|
||||
@@ -491,7 +525,7 @@ impl System {
|
||||
let local_info = self.local_status.load();
|
||||
|
||||
if local_info.replication_factor < info.replication_factor {
|
||||
error!("Some node have a higher replication factor ({}) than this one ({}). This is not supported and might lead to bugs",
|
||||
error!("Some node have a higher replication factor ({}) than this one ({}). This is not supported and will lead to data corruption. Shutting down for safety.",
|
||||
info.replication_factor,
|
||||
local_info.replication_factor);
|
||||
std::process::exit(1);
|
||||
@@ -519,6 +553,16 @@ impl System {
|
||||
self: &Arc<Self>,
|
||||
adv: &ClusterLayout,
|
||||
) -> Result<SystemRpc, Error> {
|
||||
if adv.replication_factor != self.replication_factor {
|
||||
let msg = format!(
|
||||
"Received a cluster layout from another node with replication factor {}, which is different from what we have in our configuration ({}). Discarding the cluster layout we received.",
|
||||
adv.replication_factor,
|
||||
self.replication_factor
|
||||
);
|
||||
error!("{}", msg);
|
||||
return Err(Error::Message(msg));
|
||||
}
|
||||
|
||||
let update_ring = self.update_ring.lock().await;
|
||||
let mut layout: ClusterLayout = self.ring.borrow().layout.clone();
|
||||
|
||||
@@ -544,7 +588,7 @@ impl System {
|
||||
SystemRpc::AdvertiseClusterLayout(layout),
|
||||
RequestStrategy::with_priority(PRIO_HIGH),
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
Ok(())
|
||||
});
|
||||
self.background.spawn(self.clone().save_cluster_layout());
|
||||
@@ -559,11 +603,12 @@ impl System {
|
||||
|
||||
self.update_local_status();
|
||||
let local_status: NodeStatus = self.local_status.load().as_ref().clone();
|
||||
self.rpc
|
||||
let _ = self
|
||||
.rpc
|
||||
.broadcast(
|
||||
&self.system_endpoint,
|
||||
SystemRpc::AdvertiseStatus(local_status),
|
||||
RequestStrategy::with_priority(PRIO_HIGH).with_timeout(PING_TIMEOUT),
|
||||
RequestStrategy::with_priority(PRIO_HIGH),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -589,7 +634,7 @@ impl System {
|
||||
if not_configured || no_peers || bad_peers {
|
||||
info!("Doing a bootstrap/discovery step (not_configured: {}, no_peers: {}, bad_peers: {})", not_configured, no_peers, bad_peers);
|
||||
|
||||
let mut ping_list = self.bootstrap_peers.clone();
|
||||
let mut ping_list = resolve_peers(&self.bootstrap_peers).await;
|
||||
|
||||
// Add peer list from list stored on disk
|
||||
if let Ok(peers) = self.persist_peer_list.load_async().await {
|
||||
@@ -687,7 +732,7 @@ impl System {
|
||||
&self.system_endpoint,
|
||||
peer,
|
||||
SystemRpc::PullClusterLayout,
|
||||
RequestStrategy::with_priority(PRIO_HIGH).with_timeout(PING_TIMEOUT),
|
||||
RequestStrategy::with_priority(PRIO_HIGH),
|
||||
)
|
||||
.await;
|
||||
if let Ok(SystemRpc::AdvertiseClusterLayout(layout)) = resp {
|
||||
@@ -720,6 +765,25 @@ fn get_default_ip() -> Option<IpAddr> {
|
||||
.map(|a| a.ip())
|
||||
}
|
||||
|
||||
async fn resolve_peers(peers: &[String]) -> Vec<(NodeID, SocketAddr)> {
|
||||
let mut ret = vec![];
|
||||
|
||||
for peer in peers.iter() {
|
||||
match parse_and_resolve_peer_addr_async(peer).await {
|
||||
Some((pubkey, addrs)) => {
|
||||
for ip in addrs {
|
||||
ret.push((pubkey, ip));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!("Unable to parse and/or resolve peer hostname {}", peer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
struct ConsulDiscoveryParam {
|
||||
consul_host: String,
|
||||
service_name: String,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_table"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
@@ -15,13 +15,14 @@ path = "lib.rs"
|
||||
|
||||
[dependencies]
|
||||
garage_db = { version = "0.8.0", path = "../db" }
|
||||
garage_rpc = { version = "0.7.0", path = "../rpc" }
|
||||
garage_util = { version = "0.7.0", path = "../util" }
|
||||
garage_rpc = { version = "0.8.0", path = "../rpc" }
|
||||
garage_util = { version = "0.8.0", path = "../util" }
|
||||
|
||||
opentelemetry = "0.17"
|
||||
|
||||
async-trait = "0.1.7"
|
||||
bytes = "1.0"
|
||||
hex = "0.4"
|
||||
hexdump = "0.1"
|
||||
tracing = "0.1.30"
|
||||
rand = "0.8"
|
||||
|
||||
+2
-7
@@ -25,7 +25,6 @@ use crate::replication::*;
|
||||
use crate::schema::*;
|
||||
|
||||
const TABLE_GC_BATCH_SIZE: usize = 1024;
|
||||
const TABLE_GC_RPC_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
// GC delay for table entries: 1 day (24 hours)
|
||||
// (the delay before the entry is added in the GC todo list
|
||||
@@ -236,9 +235,7 @@ where
|
||||
&self.endpoint,
|
||||
&nodes[..],
|
||||
GcRpc::Update(updates),
|
||||
RequestStrategy::with_priority(PRIO_BACKGROUND)
|
||||
.with_quorum(nodes.len())
|
||||
.with_timeout(TABLE_GC_RPC_TIMEOUT),
|
||||
RequestStrategy::with_priority(PRIO_BACKGROUND).with_quorum(nodes.len()),
|
||||
)
|
||||
.await
|
||||
.err_context("GC: send tombstones")?;
|
||||
@@ -259,9 +256,7 @@ where
|
||||
&self.endpoint,
|
||||
&nodes[..],
|
||||
GcRpc::DeleteIfEqualHash(deletes),
|
||||
RequestStrategy::with_priority(PRIO_BACKGROUND)
|
||||
.with_quorum(nodes.len())
|
||||
.with_timeout(TABLE_GC_RPC_TIMEOUT),
|
||||
RequestStrategy::with_priority(PRIO_BACKGROUND).with_quorum(nodes.len()),
|
||||
)
|
||||
.await
|
||||
.err_context("GC: remote delete tombstones")?;
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ pub trait Entry<P: PartitionKey, S: SortKey>:
|
||||
}
|
||||
|
||||
/// Trait for the schema used in a table
|
||||
pub trait TableSchema: Send + Sync {
|
||||
pub trait TableSchema: Send + Sync + 'static {
|
||||
/// The name of the table in the database
|
||||
const TABLE_NAME: &'static str;
|
||||
|
||||
|
||||
+6
-13
@@ -24,8 +24,6 @@ use crate::merkle::*;
|
||||
use crate::replication::*;
|
||||
use crate::*;
|
||||
|
||||
const TABLE_SYNC_RPC_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
// Do anti-entropy every 10 minutes
|
||||
const ANTI_ENTROPY_INTERVAL: Duration = Duration::from_secs(10 * 60);
|
||||
|
||||
@@ -247,9 +245,7 @@ where
|
||||
&self.endpoint,
|
||||
nodes,
|
||||
SyncRpc::Items(values),
|
||||
RequestStrategy::with_priority(PRIO_BACKGROUND)
|
||||
.with_quorum(nodes.len())
|
||||
.with_timeout(TABLE_SYNC_RPC_TIMEOUT),
|
||||
RequestStrategy::with_priority(PRIO_BACKGROUND).with_quorum(nodes.len()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -310,8 +306,7 @@ where
|
||||
&self.endpoint,
|
||||
who,
|
||||
SyncRpc::RootCkHash(partition.partition, root_ck_hash),
|
||||
RequestStrategy::with_priority(PRIO_BACKGROUND)
|
||||
.with_timeout(TABLE_SYNC_RPC_TIMEOUT),
|
||||
RequestStrategy::with_priority(PRIO_BACKGROUND),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -350,11 +345,11 @@ where
|
||||
// Just send that item directly
|
||||
if let Some(val) = self.data.store.get(&ik[..])? {
|
||||
if blake2sum(&val[..]) != ivhash {
|
||||
warn!("({}) Hashes differ between stored value and Merkle tree, key: {:?} (if your server is very busy, don't worry, this happens when the Merkle tree can't be updated fast enough)", F::TABLE_NAME, ik);
|
||||
debug!("({}) Hashes differ between stored value and Merkle tree, key: {} (if your server is very busy, don't worry, this happens when the Merkle tree can't be updated fast enough)", F::TABLE_NAME, hex::encode(ik));
|
||||
}
|
||||
todo_items.push(val.to_vec());
|
||||
} else {
|
||||
warn!("({}) Item from Merkle tree not found in store: {:?} (if your server is very busy, don't worry, this happens when the Merkle tree can't be updated fast enough)", F::TABLE_NAME, ik);
|
||||
debug!("({}) Item from Merkle tree not found in store: {} (if your server is very busy, don't worry, this happens when the Merkle tree can't be updated fast enough)", F::TABLE_NAME, hex::encode(ik));
|
||||
}
|
||||
}
|
||||
MerkleNode::Intermediate(l) => {
|
||||
@@ -367,8 +362,7 @@ where
|
||||
&self.endpoint,
|
||||
who,
|
||||
SyncRpc::GetNode(key.clone()),
|
||||
RequestStrategy::with_priority(PRIO_BACKGROUND)
|
||||
.with_timeout(TABLE_SYNC_RPC_TIMEOUT),
|
||||
RequestStrategy::with_priority(PRIO_BACKGROUND),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
@@ -444,8 +438,7 @@ where
|
||||
&self.endpoint,
|
||||
who,
|
||||
SyncRpc::Items(values),
|
||||
RequestStrategy::with_priority(PRIO_BACKGROUND)
|
||||
.with_timeout(TABLE_SYNC_RPC_TIMEOUT),
|
||||
RequestStrategy::with_priority(PRIO_BACKGROUND),
|
||||
)
|
||||
.await?;
|
||||
if let SyncRpc::Ok = rpc_resp {
|
||||
|
||||
+3
-12
@@ -1,7 +1,6 @@
|
||||
use std::borrow::Borrow;
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures::stream::*;
|
||||
@@ -31,8 +30,6 @@ use crate::schema::*;
|
||||
use crate::sync::*;
|
||||
use crate::util::*;
|
||||
|
||||
pub const TABLE_RPC_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
pub struct Table<F: TableSchema + 'static, R: TableReplication + 'static> {
|
||||
pub system: Arc<System>,
|
||||
pub data: Arc<TableData<F, R>>,
|
||||
@@ -113,7 +110,6 @@ where
|
||||
async fn insert_internal(&self, e: &F::E) -> Result<(), Error> {
|
||||
let hash = e.partition_key().hash();
|
||||
let who = self.data.replication.write_nodes(&hash);
|
||||
//eprintln!("insert who: {:?}", who);
|
||||
|
||||
let e_enc = Arc::new(ByteBuf::from(rmp_to_vec_all_named(e)?));
|
||||
let rpc = TableRpc::<F>::Update(vec![e_enc]);
|
||||
@@ -125,8 +121,7 @@ where
|
||||
&who[..],
|
||||
rpc,
|
||||
RequestStrategy::with_priority(PRIO_NORMAL)
|
||||
.with_quorum(self.data.replication.write_quorum())
|
||||
.with_timeout(TABLE_RPC_TIMEOUT),
|
||||
.with_quorum(self.data.replication.write_quorum()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -178,7 +173,7 @@ where
|
||||
&self.endpoint,
|
||||
node,
|
||||
rpc,
|
||||
RequestStrategy::with_priority(PRIO_NORMAL).with_timeout(TABLE_RPC_TIMEOUT),
|
||||
RequestStrategy::with_priority(PRIO_NORMAL),
|
||||
)
|
||||
.await?;
|
||||
Ok::<_, Error>((node, resp))
|
||||
@@ -235,7 +230,6 @@ where
|
||||
rpc,
|
||||
RequestStrategy::with_priority(PRIO_NORMAL)
|
||||
.with_quorum(self.data.replication.read_quorum())
|
||||
.with_timeout(TABLE_RPC_TIMEOUT)
|
||||
.interrupt_after_quorum(true),
|
||||
)
|
||||
.await?;
|
||||
@@ -330,7 +324,6 @@ where
|
||||
rpc,
|
||||
RequestStrategy::with_priority(PRIO_NORMAL)
|
||||
.with_quorum(self.data.replication.read_quorum())
|
||||
.with_timeout(TABLE_RPC_TIMEOUT)
|
||||
.interrupt_after_quorum(true),
|
||||
)
|
||||
.await?;
|
||||
@@ -407,9 +400,7 @@ where
|
||||
&self.endpoint,
|
||||
who,
|
||||
TableRpc::<F>::Update(vec![what_enc]),
|
||||
RequestStrategy::with_priority(PRIO_NORMAL)
|
||||
.with_quorum(who.len())
|
||||
.with_timeout(TABLE_RPC_TIMEOUT),
|
||||
RequestStrategy::with_priority(PRIO_NORMAL).with_quorum(who.len()),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
|
||||
+9
-5
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_util"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
@@ -16,14 +16,19 @@ path = "lib.rs"
|
||||
[dependencies]
|
||||
garage_db = { version = "0.8.0", path = "../db" }
|
||||
|
||||
arc-swap = "1.0"
|
||||
async-trait = "0.1"
|
||||
blake2 = "0.9"
|
||||
bytes = "1.0"
|
||||
digest = "0.10"
|
||||
err-derive = "0.3"
|
||||
git-version = "0.3.4"
|
||||
xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] }
|
||||
hex = "0.4"
|
||||
lazy_static = "1.4"
|
||||
tracing = "0.1.30"
|
||||
rand = "0.8"
|
||||
sha2 = "0.9"
|
||||
sha2 = "0.10"
|
||||
|
||||
chrono = "0.4"
|
||||
rmp-serde = "0.15"
|
||||
@@ -34,14 +39,13 @@ toml = "0.5"
|
||||
futures = "0.3"
|
||||
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
||||
|
||||
#netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
|
||||
#netapp = { version = "0.4", path = "../../../netapp" }
|
||||
netapp = "0.4"
|
||||
netapp = "0.5"
|
||||
|
||||
http = "0.2"
|
||||
hyper = "0.14"
|
||||
|
||||
opentelemetry = { version = "0.17", features = [ "rt-tokio", "metrics", "trace" ] }
|
||||
|
||||
|
||||
[features]
|
||||
k2v = []
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
use bytes::Bytes;
|
||||
use digest::Digest;
|
||||
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::data::*;
|
||||
|
||||
/// Compute the sha256 of a slice,
|
||||
/// spawning on a tokio thread for CPU-intensive processing
|
||||
/// The argument has to be an owned Bytes, as it is moved out to a new thread.
|
||||
pub async fn async_sha256sum(data: Bytes) -> Hash {
|
||||
tokio::task::spawn_blocking(move || sha256sum(&data))
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Compute the blake2sum of a slice,
|
||||
/// spawning on a tokio thread for CPU-intensive processing.
|
||||
/// The argument has to be an owned Bytes, as it is moved out to a new thread.
|
||||
pub async fn async_blake2sum(data: Bytes) -> Hash {
|
||||
tokio::task::spawn_blocking(move || blake2sum(&data))
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
// ----
|
||||
|
||||
pub struct AsyncHasher<D: Digest> {
|
||||
sendblk: mpsc::Sender<Bytes>,
|
||||
task: JoinHandle<digest::Output<D>>,
|
||||
}
|
||||
|
||||
impl<D: Digest> AsyncHasher<D> {
|
||||
pub fn new() -> Self {
|
||||
let (sendblk, mut recvblk) = mpsc::channel::<Bytes>(1);
|
||||
let task = tokio::task::spawn_blocking(move || {
|
||||
let mut digest = D::new();
|
||||
while let Some(blk) = recvblk.blocking_recv() {
|
||||
digest.update(&blk[..]);
|
||||
}
|
||||
digest.finalize()
|
||||
});
|
||||
Self { sendblk, task }
|
||||
}
|
||||
|
||||
pub async fn update(&self, b: Bytes) {
|
||||
self.sendblk.send(b).await.unwrap();
|
||||
}
|
||||
|
||||
pub async fn finalize(self) -> digest::Output<D> {
|
||||
drop(self.sendblk);
|
||||
self.task.await.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Digest> Default for AsyncHasher<D> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ use futures::StreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::select;
|
||||
use tokio::sync::{mpsc, watch};
|
||||
use tracing::*;
|
||||
|
||||
use crate::background::WorkerInfo;
|
||||
use crate::error::Error;
|
||||
|
||||
+10
-36
@@ -3,12 +3,8 @@ use std::io::Read;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::de::Error as SerdeError;
|
||||
use serde::{de, Deserialize};
|
||||
|
||||
use netapp::util::parse_and_resolve_peer_addr;
|
||||
use netapp::NodeID;
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
/// Represent the whole configuration
|
||||
@@ -23,10 +19,6 @@ pub struct Config {
|
||||
#[serde(default = "default_block_size")]
|
||||
pub block_size: usize,
|
||||
|
||||
/// Size of data blocks to save to disk
|
||||
#[serde(default = "default_block_manager_background_tranquility")]
|
||||
pub block_manager_background_tranquility: u32,
|
||||
|
||||
/// Replication mode. Supported values:
|
||||
/// - none, 1 -> no replication
|
||||
/// - 2 -> 2-way replication
|
||||
@@ -47,11 +39,16 @@ pub struct Config {
|
||||
/// Address to bind for RPC
|
||||
pub rpc_bind_addr: SocketAddr,
|
||||
/// Public IP address of this node
|
||||
pub rpc_public_addr: Option<SocketAddr>,
|
||||
pub rpc_public_addr: Option<String>,
|
||||
|
||||
/// Timeout for Netapp's ping messagess
|
||||
pub rpc_ping_timeout_msec: Option<u64>,
|
||||
/// Timeout for Netapp RPC calls
|
||||
pub rpc_timeout_msec: Option<u64>,
|
||||
|
||||
/// Bootstrap peers RPC address
|
||||
#[serde(deserialize_with = "deserialize_vec_addr", default)]
|
||||
pub bootstrap_peers: Vec<(NodeID, SocketAddr)>,
|
||||
#[serde(default)]
|
||||
pub bootstrap_peers: Vec<String>,
|
||||
/// Consul host to connect to to discover more peers
|
||||
pub consul_host: Option<String>,
|
||||
/// Consul service name to use
|
||||
@@ -81,11 +78,10 @@ pub struct Config {
|
||||
pub s3_api: S3ApiConfig,
|
||||
|
||||
/// Configuration for K2V api
|
||||
#[cfg(feature = "k2v")]
|
||||
pub k2v_api: Option<K2VApiConfig>,
|
||||
|
||||
/// Configuration for serving files as normal web server
|
||||
pub s3_web: WebConfig,
|
||||
pub s3_web: Option<WebConfig>,
|
||||
|
||||
/// Configuration for the admin API endpoint
|
||||
#[serde(default = "Default::default")]
|
||||
@@ -96,7 +92,7 @@ pub struct Config {
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct S3ApiConfig {
|
||||
/// Address and port to bind for api serving
|
||||
pub api_bind_addr: SocketAddr,
|
||||
pub api_bind_addr: Option<SocketAddr>,
|
||||
/// S3 region to use
|
||||
pub s3_region: String,
|
||||
/// Suffix to remove from domain name to find bucket. If None,
|
||||
@@ -105,7 +101,6 @@ pub struct S3ApiConfig {
|
||||
}
|
||||
|
||||
/// Configuration for K2V api
|
||||
#[cfg(feature = "k2v")]
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct K2VApiConfig {
|
||||
/// Address and port to bind for api serving
|
||||
@@ -147,9 +142,6 @@ fn default_sled_flush_every_ms() -> u64 {
|
||||
fn default_block_size() -> usize {
|
||||
1048576
|
||||
}
|
||||
fn default_block_manager_background_tranquility() -> u32 {
|
||||
2
|
||||
}
|
||||
|
||||
/// Read and parse configuration
|
||||
pub fn read_config(config_file: PathBuf) -> Result<Config, Error> {
|
||||
@@ -163,24 +155,6 @@ pub fn read_config(config_file: PathBuf) -> Result<Config, Error> {
|
||||
Ok(toml::from_str(&config)?)
|
||||
}
|
||||
|
||||
fn deserialize_vec_addr<'de, D>(deserializer: D) -> Result<Vec<(NodeID, SocketAddr)>, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
let mut ret = vec![];
|
||||
|
||||
for peer in <Vec<&str>>::deserialize(deserializer)? {
|
||||
let (pubkey, addrs) = parse_and_resolve_peer_addr(peer).ok_or_else(|| {
|
||||
D::Error::custom(format!("Unable to parse or resolve peer: {}", peer))
|
||||
})?;
|
||||
for ip in addrs {
|
||||
ret.push((pubkey, ip));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
fn default_compression() -> Option<i32> {
|
||||
Some(1)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::crdt::crdt::*;
|
||||
|
||||
/// Boolean, where `true` is an absorbing state
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct Bool(bool);
|
||||
|
||||
impl Bool {
|
||||
|
||||
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::crdt::crdt::*;
|
||||
|
||||
/// Deletable object (once deleted, cannot go back)
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum Deletable<T> {
|
||||
Present(T),
|
||||
Deleted,
|
||||
|
||||
@@ -37,7 +37,7 @@ use crate::crdt::crdt::*;
|
||||
///
|
||||
/// This scheme is used by AWS S3 or Soundcloud and often without knowing
|
||||
/// in enterprise when reconciliating databases with ad-hoc scripts.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct Lww<T> {
|
||||
ts: u64,
|
||||
v: T,
|
||||
|
||||
@@ -23,7 +23,7 @@ use crate::crdt::crdt::*;
|
||||
/// However, note that even if we were using a more efficient data structure such as a `BTreeMap`,
|
||||
/// the serialization cost `O(n)` would still have to be paid at each modification, so we are
|
||||
/// actually not losing anything here.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct LwwMap<K, V> {
|
||||
vals: Vec<(K, u64, V)>,
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user