Compare commits

..

22 Commits

Author SHA1 Message Date
Alex Auvolat b9127dd6f8 Prepare for v0.3.0 and add migration path from v0.2.1.x 2021-05-28 15:29:58 +02:00
Alex Auvolat ddb2b29bfd Rename datacenters into zones (doc not yet updated) 2021-05-28 14:07:36 +02:00
Alex Auvolat b490ebc7f6 Many improvements on ring/replication and its configuration:
- Explicit "replication_mode" configuration parameters that takes
  either "none", "2" or "3" as values, instead of letting user configure
  replication factor themselves. These are presets whose corresponding
  replication/quorum values can be found in replication/mode.rs

- Explicit support for single-node and two-node deployments
  (number of nodes must be at least "replication_mode", with "none"
  we can have only one node)

- Ring is now stored much more compactly with 256*8 + n*32 bytes,
  instead of 256*32 bytes

- Support for gateway-only nodes that do not store data
  (these nodes still need a metadata_directory to store the list
  of bucket and keys since those are stored on all nodes; it also
  technically needs a data_directory to start but it will stay
  empty unless we have bugs)
2021-05-28 14:07:36 +02:00
Quentin Dufour c8aa1eb481 Add preliminary support for Duck 2021-05-15 10:24:20 +02:00
Quentin Dufour 5fdabf3e75 Add basic support for the "Versioning" command 2021-05-14 22:33:26 +02:00
Alex Auvolat 6ccffc3162 Improved XML serialization
- Use quick_xml and serde for all XML response returned by the S3 API.
- Include tests for all structs used to generate XML
- Remove old manual XML escaping function which was unsafe
2021-05-06 22:37:15 +02:00
Trinity Pointard e4b9e4e24d rename types to CamelCase 2021-05-03 22:15:09 +02:00
Trinity Pointard 6644df6b96 fix clippy warnings on garage 2021-05-03 22:11:42 +02:00
Trinity Pointard f8ae8fc4be fix clippy warnings on web 2021-05-03 22:11:42 +02:00
Trinity Pointard 84856e84e5 fix clippy warnings on api 2021-05-03 22:11:41 +02:00
Trinity Pointard 4a1e079e8f fix clippy warnings on model 2021-05-03 22:11:41 +02:00
Trinity Pointard f5a0cf0414 fix clippy warnings on table 2021-05-03 22:11:41 +02:00
Trinity Pointard f05bb111c2 fix clippy warnings on util and rpc 2021-05-03 22:11:41 +02:00
Trinity Pointard 88925ebe22 add clippy to the CI 2021-05-03 22:11:37 +02:00
Quentin Dufour 631c36b3ff S3 API: support ListBuckets 2021-05-03 21:55:30 +02:00
Alex Auvolat ee2a3d363b Remove STREAMING-AWS4-HMAC-SHA256-PAYLOAD (see #64) 2021-05-03 17:30:40 +02:00
Alex Auvolat 575726358c Tune Sled configuration
- Make sled cache size and flush interval configurable
- Set less agressive default values:
  - cache size 128MB instead of 1GB
  - Flush interval 2 seconds instead of .5 seconds
2021-05-03 17:27:43 +02:00
Alex Auvolat 339c611789 Add links to git/matrix/drone on top of documentation 2021-05-03 10:45:45 +02:00
Quentin Dufour ef4d6e782a Add minio & rclone to our functional tests
It is now possible to configure which clients
you do not want to test with the env variable SKIP_XXX=1,
XXX being the client name. eg. SKIP_S3CMD=1 ./script/test-smoke.sh
2021-05-02 14:59:58 +02:00
Alex Auvolat a981244f11 Replace talk links with permalinks 2021-04-30 15:51:32 +02:00
Alex b1f60579a1 Merge pull request 'created doc/talks subfolder and added my talk and the previous one done at the wide team' (#67) from talks into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/67
2021-04-30 15:47:11 +02:00
LUXEY Adrien be3b1d8f91 created doc/talks subfolder and added my talk and the previous one done at the wide team 2021-04-30 15:37:43 +02:00
119 changed files with 4144 additions and 995 deletions
+15 -5
View File
@@ -36,8 +36,19 @@ steps:
branch: branch:
- nonexistent_skip_this_step - nonexistent_skip_this_step
- name: code quality
image: superboum/garage_builder_amd64:4
volumes:
- name: cargo_home
path: /drone/cargo
environment:
CARGO_HOME: /drone/cargo
commands:
- cargo fmt -- --check
- cargo clippy -- --deny warnings
- name: build - name: build
image: lxpz/garage_builder_amd64:1 image: superboum/garage_builder_amd64:4
volumes: volumes:
- name: cargo_home - name: cargo_home
path: /drone/cargo path: /drone/cargo
@@ -45,11 +56,10 @@ steps:
CARGO_HOME: /drone/cargo CARGO_HOME: /drone/cargo
commands: commands:
- pwd - pwd
- cargo fmt -- --check
- cargo build - cargo build
- name: cargo-test - name: cargo-test
image: lxpz/garage_builder_amd64:1 image: superboum/garage_builder_amd64:4
volumes: volumes:
- name: cargo_home - name: cargo_home
path: /drone/cargo path: /drone/cargo
@@ -85,7 +95,7 @@ steps:
- nonexistent_skip_this_step - nonexistent_skip_this_step
- name: smoke-test - name: smoke-test
image: lxpz/garage_builder_amd64:1 image: superboum/garage_builder_amd64:4
volumes: volumes:
- name: cargo_home - name: cargo_home
path: /drone/cargo path: /drone/cargo
@@ -129,6 +139,6 @@ steps:
--- ---
kind: signature kind: signature
hmac: de82026387bd09e547dbc9cc5d232fd865204b4f393d32508c50b58f8e60611d hmac: e919f8a66d20ebfeeec56b291a8a0fdd59a482601da987fcf533d96d24768744
... ...
Generated
+81 -14
View File
@@ -350,16 +350,16 @@ dependencies = [
[[package]] [[package]]
name = "garage" name = "garage"
version = "0.2.1" version = "0.3.0"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures", "futures",
"futures-util", "futures-util",
"garage_api", "garage_api",
"garage_model", "garage_model",
"garage_rpc", "garage_rpc 0.3.0",
"garage_table", "garage_table",
"garage_util", "garage_util 0.3.0",
"garage_web", "garage_web",
"git-version", "git-version",
"hex", "hex",
@@ -376,7 +376,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_api" name = "garage_api"
version = "0.2.1" version = "0.3.0"
dependencies = [ dependencies = [
"base64", "base64",
"bytes", "bytes",
@@ -387,7 +387,7 @@ dependencies = [
"futures-util", "futures-util",
"garage_model", "garage_model",
"garage_table", "garage_table",
"garage_util", "garage_util 0.3.0",
"hex", "hex",
"hmac", "hmac",
"http", "http",
@@ -397,7 +397,9 @@ dependencies = [
"log", "log",
"md-5", "md-5",
"percent-encoding", "percent-encoding",
"quick-xml",
"roxmltree", "roxmltree",
"serde",
"sha2", "sha2",
"tokio", "tokio",
"url", "url",
@@ -405,14 +407,14 @@ dependencies = [
[[package]] [[package]]
name = "garage_model" name = "garage_model"
version = "0.2.1" version = "0.3.0"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"futures", "futures",
"futures-util", "futures-util",
"garage_rpc", "garage_rpc 0.3.0",
"garage_table", "garage_table",
"garage_util", "garage_util 0.3.0",
"hex", "hex",
"log", "log",
"rand", "rand",
@@ -426,12 +428,40 @@ dependencies = [
[[package]] [[package]]
name = "garage_rpc" name = "garage_rpc"
version = "0.2.1" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c182633cebe4abed9594afb14770fc45402513765d38a4b19659ae0ccb2a2f"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"bytes", "bytes",
"futures", "futures",
"futures-util", "futures-util",
"garage_util", "garage_util 0.2.1",
"gethostname",
"hex",
"http",
"hyper",
"hyper-rustls",
"log",
"rmp-serde",
"rustls",
"serde",
"serde_json",
"tokio",
"tokio-rustls",
"tokio-stream",
"webpki",
]
[[package]]
name = "garage_rpc"
version = "0.3.0"
dependencies = [
"arc-swap",
"bytes",
"futures",
"futures-util",
"garage_rpc 0.2.1",
"garage_util 0.3.0",
"gethostname", "gethostname",
"hex", "hex",
"http", "http",
@@ -450,13 +480,13 @@ dependencies = [
[[package]] [[package]]
name = "garage_table" name = "garage_table"
version = "0.2.1" version = "0.3.0"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures", "futures",
"futures-util", "futures-util",
"garage_rpc", "garage_rpc 0.3.0",
"garage_util", "garage_util 0.3.0",
"hexdump", "hexdump",
"log", "log",
"rand", "rand",
@@ -470,6 +500,33 @@ dependencies = [
[[package]] [[package]]
name = "garage_util" name = "garage_util"
version = "0.2.1" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aef76d3779e406a16fdcaffe8d86b8ae2943a549d2b33f2c20930838764464c0"
dependencies = [
"blake2",
"chrono",
"err-derive",
"futures",
"hex",
"http",
"hyper",
"log",
"rand",
"rmp-serde",
"rustls",
"serde",
"serde_json",
"sha2",
"sled",
"tokio",
"toml",
"webpki",
"xxhash-rust",
]
[[package]]
name = "garage_util"
version = "0.3.0"
dependencies = [ dependencies = [
"blake2", "blake2",
"chrono", "chrono",
@@ -494,14 +551,14 @@ dependencies = [
[[package]] [[package]]
name = "garage_web" name = "garage_web"
version = "0.2.1" version = "0.3.0"
dependencies = [ dependencies = [
"err-derive", "err-derive",
"futures", "futures",
"garage_api", "garage_api",
"garage_model", "garage_model",
"garage_table", "garage_table",
"garage_util", "garage_util 0.3.0",
"http", "http",
"hyper", "hyper",
"idna", "idna",
@@ -1034,6 +1091,16 @@ version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-xml"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0452695941410a58c8ce4391707ba9bad26a247173bd9886a05a5e8a8babec75"
dependencies = [
"memchr",
"serde",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.9" version = "1.0.9"
+1 -16
View File
@@ -1,20 +1,5 @@
BIN=target/release/garage
DOCKER=lxpz/garage_amd64
all: all:
clear; cargo build clear; cargo build
$(BIN): release:
RUSTFLAGS="-C link-arg=-fuse-ld=lld -C target-cpu=x86-64 -C target-feature=+sse2" cargo build --release --no-default-features RUSTFLAGS="-C link-arg=-fuse-ld=lld -C target-cpu=x86-64 -C target-feature=+sse2" cargo build --release --no-default-features
$(BIN).stripped: $(BIN)
cp $^ $@
strip $@
docker: $(BIN).stripped
docker pull archlinux:latest
docker build -t $(DOCKER):$(TAG) .
docker push $(DOCKER):$(TAG)
docker tag $(DOCKER):$(TAG) $(DOCKER):latest
docker push $(DOCKER):latest
-22
View File
@@ -1,22 +0,0 @@
block_size = 1048576 # objects are split in blocks of maximum this number of bytes
metadata_dir = "/tmp/garage-meta"
data_dir = "/tmp/garage-data"
rpc_bind_addr = "[::]:3901" # the port other Garage nodes will use to talk to this node
bootstrap_peers = []
max_concurrent_rpc_requests = 12
data_replication_factor = 3
meta_replication_factor = 3
meta_epidemic_fanout = 3
[s3_api]
api_bind_addr = "[::1]:3900" # the S3 API port, HTTP without TLS. Add a reverse proxy for the TLS part.
s3_region = "garage" # set this to anything. S3 API calls will fail if they are not made against the region set here.
[s3_web]
bind_addr = "[::1]:3902"
root_domain = ".garage.tld"
index = "index.html"
+10 -1
View File
@@ -4,6 +4,13 @@
</a> </a>
</p> </p>
<p align="center" style="text-align:center;">
[ <a href="https://git.deuxfleurs.fr/Deuxfleurs/garage">Git repository</a>
| <a href="https://matrix.to/#/%23garage:deuxfleurs.fr">Matrix channel</a>
| <a href="https://drone.deuxfleurs.fr/Deuxfleurs/garage">Drone CI</a>
]
</p>
``` ```
This very website is hosted using Garage. In other words: the doc is the PoC! This very website is hosted using Garage. In other words: the doc is the PoC!
``` ```
@@ -89,7 +96,9 @@ If you encounter a specific bug in Garage or plan to patch it, you may jump dire
We love to talk and hear about Garage, that's why we keep a log here: We love to talk and hear about Garage, that's why we keep a log here:
- [(fr, 2020-12-02) Garage : jouer dans la cour des grands quand on est un hébergeur associatif](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/main/doc/20201202_talk/talk.pdf) - [(en, 2021-04-28) Distributed object storage is centralised](https://git.deuxfleurs.fr/Deuxfleurs/garage/raw/commit/b1f60579a13d3c5eba7f74b1775c84639ea9b51a/doc/talks/2021-04-28_spirals-team/talk.pdf)
- [(fr, 2020-12-02) Garage : jouer dans la cour des grands quand on est un hébergeur associatif](https://git.deuxfleurs.fr/Deuxfleurs/garage/raw/commit/b1f60579a13d3c5eba7f74b1775c84639ea9b51a/doc/talks/2020-12-02_wide-team/talk.pdf)
*Did you write or talk about Garage? [Open a pull request](https://git.deuxfleurs.fr/Deuxfleurs/garage/) to add a link here!* *Did you write or talk about Garage? [Open a pull request](https://git.deuxfleurs.fr/Deuxfleurs/garage/) to add a link here!*

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Before

Width:  |  Height:  |  Size: 360 KiB

After

Width:  |  Height:  |  Size: 360 KiB

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

@@ -0,0 +1,10 @@
*.bbl
*.aux
*.bcf
*.blg
*.log
*.run.xml
*.synctex.gz
build/
main.pdf
figures/crdt.pdf
@@ -0,0 +1,33 @@
.PHONY: all viewpdf pdf clean
TARGET = main
SOURCE_FILES = $(shell find . -type f -name "*.tex" -print)
CLASS_FILES = $(shell find . -type f -name "*.cls" -print)
BIB_FILES = $(shell find . -type f -name "*.bib" -print)
FIGURES = $(shell find . -path "./figures/*" -type f -print)
BUILD_PATH = build
BUILD_FILES = $(shell find $(BUILD_PATH) -type f -print)
BIB_PROCESSOR := biber
.PHONY: all pdf clean figures
all: pdf
pdf: $(TARGET).pdf
clean:
@rm $(TARGET).pdf $(BUILD_FILES) > /dev/null 2>&1 || exit 0
figures: figures/crdt.pdf
$(TARGET).pdf: figures $(FIGURES) $(SOURCE_FILES) $(BIB_FILES) $(CLASS_FILES)
@mkdir -p $(BUILD_PATH) > /dev/null 2>&1 || exit 0
@pdflatex -interaction=nonstopmode -jobname=$(TARGET) -output-directory $(BUILD_PATH) $(TARGET).tex
@$(BIB_PROCESSOR) --output-directory $(BUILD_PATH) $(TARGET)
@pdflatex -interaction=nonstopmode -jobname=$(TARGET) -output-directory $(BUILD_PATH) $(TARGET).tex # For biber
@pdflatex -interaction=nonstopmode -jobname=$(TARGET) -output-directory $(BUILD_PATH) $(TARGET).tex # For biber
@ln -fs $(BUILD_PATH)/$(TARGET).pdf $(TARGET).pdf
figures/crdt.pdf: figures/svg/crdt.svg
@inkscape -C --file=$< --export-pdf=$@
@@ -0,0 +1 @@
Presentation of Garage by Adrien on April, 28th 2021, for his research team [Spirals](https://team.inria.fr/spirals/).
@@ -0,0 +1,27 @@
@inproceedings{brewer_towards_2000,
title = {Towards {{Robust Distributed Systems}}},
booktitle = {{{ACM PODC}}},
author = {Brewer, Eric},
year = {2000}
}
@incollection{defago_conflict-free_2011,
title = {Conflict-{{Free Replicated Data Types}}},
booktitle = {Stabilization, {{Safety}}, and {{Security}} of {{Distributed Systems}}},
author = {Shapiro, Marc and Pregui{\c c}a, Nuno and Baquero, Carlos and Zawirski, Marek},
year = {2011},
address = {{Berlin, Heidelberg}},
}
@inproceedings{decandia_dynamo:_2007,
title = {Dynamo: {{Amazon}}'s {{Highly Available Key}}-Value {{Store}}},
booktitle = {{ACM SOSP}},
author = {DeCandia, Giuseppe and Hastorun, Deniz and Jampani, Madan and Kakulapati, Gunavardhan and Lakshman, Avinash and Pilchin, Alex and Sivasubramanian, Swaminathan and Vosshall, Peter and Vogels, Werner},
year = {2007},
address = {{New York, USA}},
}
@@ -0,0 +1,7 @@
\section{Conclusion}
\begin{frame}{The future is cooler when we bend it our way}
Contributions welcome! :D
\end{frame}
@@ -0,0 +1,124 @@
\section{Escaping the cloud}
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% \begin{frame}{Down to Earth with home-hosting}
% \todo{Stanley Parabole reference?}
% \end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}{Why?}
\begin{itemize}
\item \textbf{Privacy}: no prying eyes besides your ISP
\item \textbf{Control} of your infrastructure
\item \textbf{Ecology}: reuse old hardware
\end{itemize}
\vfill
\begin{block}{\emph{Tim Berners-Lee} (1994)}
``Now, if someone tries to monopolize the Web, for example pushes proprietary variations on network protocols, then that would make me unhappy.''
\end{block}
\begin{itemize}
\item Make Tim Berners-Lee happy
\end{itemize}
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}{What?}
\centering\Large
A data store for commodity hardware on heterogenous household connections.
\vfill\raggedright\normalsize
\begin{block}{Targetting user-facing services}
\begin{itemize}
\item Static sites
\item E-mails
\item Instant communication
%\item Video streaming % No need for a data store
\item Collaboration
\end{itemize}
\end{block}
\vfill
Nothing fancy like sensors data streams, AI or IoT.
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}{What?}
\begin{block}{Requirements}
\begin{itemize}
\item \textbf{No single point of failure} / flat hierarchy:
Any node can die for extended periods of time.
\item \textbf{Multi-site}: cluster spans regions/countries.
\item \textbf{Acceptable performance}.
\item \textbf{Lightweight}: targets legacy hardware.
\item \textbf{Conceptually simple}: built for low-tech organisations.
Adding/maintaining cluster nodes should be easy.
\end{itemize}
\end{block}
\vfill
\begin{block}{Non-goals}
\begin{itemize}
\item \textbf{Super badass performance}.
\item \textbf{NAT traversal} etc.: we require full-mesh connectivity.
\end{itemize}
\end{block}
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}{How?}
\begin{itemize}
\item Theoretically possible with object storage \& CRDTs.
\vfill
\item Household uplinks are getting decent (optical fibers).
\end{itemize}
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}{Research Questions}
\begin{itemize}
\item Decent performance despite bad inter-node connectivity.
\vfill
\item Tailoring workloads as a function of nodes' capabilities:
\begin{itemize}
\item Make use of low-end nodes (e.g. Raspberry Pis),
\item Avoid impeding global performance because of low-end nodes.
\end{itemize}
\vfill
\item Building CRDTs for target use-cases:
\begin{itemize}
\item Software engineering: DSL or native code?
\item Provide APIs to data store users? Risky?
\end{itemize}
\vfill
\item Cluster management: effortless UX, low perf. overhead.
\end{itemize}
\end{frame}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

@@ -0,0 +1,930 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="119.30722mm"
height="121.49291mm"
viewBox="0 0 119.30722 121.49291"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="conflict.svg">
<defs
id="defs2">
<marker
inkscape:stockid="Arrow1Mend"
orient="auto"
refY="0"
refX="0"
id="marker2999"
style="overflow:visible"
inkscape:isstock="true">
<path
inkscape:connector-curvature="0"
id="path2997"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
transform="matrix(-0.4,0,0,-0.4,-4,0)" />
</marker>
<marker
inkscape:stockid="Arrow1Mend"
orient="auto"
refY="0"
refX="0"
id="marker2964"
style="overflow:visible"
inkscape:isstock="true">
<path
inkscape:connector-curvature="0"
id="path2962"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
transform="matrix(-0.4,0,0,-0.4,-4,0)" />
</marker>
<marker
inkscape:stockid="Arrow1Mend"
orient="auto"
refY="0"
refX="0"
id="marker1467"
style="overflow:visible"
inkscape:isstock="true">
<path
inkscape:connector-curvature="0"
id="path1465"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
transform="matrix(-0.4,0,0,-0.4,-4,0)" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker1449"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="Arrow1Mend">
<path
inkscape:connector-curvature="0"
transform="matrix(-0.4,0,0,-0.4,-4,0)"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
id="path1447" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker12071"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="Arrow1Mend"
inkscape:collect="always">
<path
transform="matrix(-0.4,0,0,-0.4,-4,0)"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
id="path12069"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker9668"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="Arrow1Mend"
inkscape:collect="always">
<path
transform="matrix(-0.4,0,0,-0.4,-4,0)"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
id="path9666"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="Arrow1Mend"
orient="auto"
refY="0"
refX="0"
id="marker6943"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path6941"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
transform="matrix(-0.4,0,0,-0.4,-4,0)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker5445"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="Arrow1Mend">
<path
transform="matrix(-0.4,0,0,-0.4,-4,0)"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
id="path5443"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="Arrow1Mend"
orient="auto"
refY="0"
refX="0"
id="marker1115"
style="overflow:visible"
inkscape:isstock="true"
inkscape:collect="always">
<path
id="path861"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
transform="matrix(-0.4,0,0,-0.4,-4,0)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="Arrow1Mend"
orient="auto"
refY="0"
refX="0"
id="Arrow1Mend"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path826"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
transform="matrix(-0.4,0,0,-0.4,-4,0)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="Arrow1Lend"
orient="auto"
refY="0"
refX="0"
id="Arrow1Lend"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path820"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
transform="matrix(-0.8,0,0,-0.8,-10,0)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="Arrow1Lstart"
orient="auto"
refY="0"
refX="0"
id="marker1101"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path1099"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
transform="matrix(0.8,0,0,0.8,10,0)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="Arrow1Lstart"
orient="auto"
refY="0"
refX="0"
id="Arrow1Lstart"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path817"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
transform="matrix(0.8,0,0,0.8,10,0)"
inkscape:connector-curvature="0" />
</marker>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.4"
inkscape:cx="244.86596"
inkscape:cy="205.06317"
inkscape:document-units="mm"
inkscape:current-layer="layer5"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1055"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:snap-global="false" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Common"
transform="translate(-43.663712,-16.067619)"
style="display:inline">
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 99.856918,34.684511 h 4.913692"
id="path1257"
inkscape:connector-curvature="0" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.79374999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Mend)"
d="M 102.31376,23.208557 V 135.9963"
id="path815"
inkscape:connector-curvature="0" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="90.524658"
y="19.57423"
id="text838"><tspan
sodipodi:role="line"
id="tspan836"
x="90.524658"
y="19.57423"
style="font-weight:bold;stroke-width:0.26458332px"># vaccines</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="46.748138"
y="19.57423"
id="text13775"><tspan
sodipodi:role="line"
id="tspan13773"
x="46.748138"
y="19.57423"
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Iwona;-inkscape-font-specification:'Iwona Italic';stroke-width:0.26458332px">Paris</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="148.20457"
y="19.57423"
id="text13798"><tspan
sodipodi:role="line"
id="tspan13796"
x="148.20457"
y="19.57423"
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Iwona;-inkscape-font-specification:'Iwona Italic';stroke-width:0.26458332px">Lille</tspan></text>
</g>
<g
inkscape:label="The problem"
inkscape:groupmode="layer"
id="layer1"
style="display:none"
transform="translate(-43.663712,-16.067619)">
<g
id="g13753"
transform="translate(0,12.170833)">
<path
inkscape:connector-curvature="0"
id="path850"
d="M 97.820644,51.619712 75.828475,64.316897"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker1115)" />
<text
y="-108.69946"
x="-183.00148"
transform="rotate(180,91.874093,54.229103)"
id="text7803"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><textPath
id="textPath7973"
xlink:href="#path850"><tspan
id="tspan7801"
style="font-weight:bold;stroke-width:0.26458332px">R</tspan></textPath></text>
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker9668)"
d="M 75.056888,72.78638 97.049057,85.483565"
id="path9498"
inkscape:connector-curvature="0" />
<text
transform="translate(9.4212508,3.3408691)"
y="2.986012"
x="8.5186663"
id="text10668"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><textPath
id="textPath10836"
xlink:href="#path9498"><tspan
id="tspan10666"
style="font-weight:bold;stroke-width:0.26458332px">W</tspan></textPath></text>
</g>
<g
id="g13763"
transform="translate(0,12.170833)">
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker12071)"
d="m 106.80689,51.619712 21.99218,12.697185"
id="path11899"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path11901"
d="M 129.57066,72.78638 107.57848,85.483565"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker9668)" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="text13737"
transform="rotate(-115.89278,108.31787,47.643413)"
x="-372.04132"
y="-217.15767"><textPath
xlink:href="#path850"
id="textPath13735"><tspan
style="font-weight:bold;stroke-width:0.26458332px"
id="tspan13733">R</tspan></textPath></text>
<text
transform="rotate(-58.856754,100.88692,38.870944)"
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="text13743"
x="46.604572"
y="6.6609678"><textPath
xlink:href="#path9498"
id="textPath13741"><tspan
style="font-weight:bold;stroke-width:0.26458332px"
id="tspan13739">W</tspan></textPath></text>
</g>
<path
inkscape:connector-curvature="0"
id="path13768"
d="m 99.856918,112.89149 h 4.913692"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="43.564934"
y="53.51746"
id="text13802"><tspan
sodipodi:role="line"
id="tspan13800"
x="43.564934"
y="53.51746"
style="stroke-width:0.26458332px">We acquired 1000 doses!</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="49.979401"
y="82.649841"
id="text13806"><tspan
sodipodi:role="line"
id="tspan13804"
x="49.979401"
y="82.649841"
style="stroke-width:0.26458332px">x + 1000</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="136.842"
y="82.649841"
id="text13810"><tspan
sodipodi:role="line"
id="tspan13808"
x="136.842"
y="82.649841"
style="stroke-width:0.26458332px">x + 500</tspan></text>
<text
id="text13814"
y="53.51746"
x="112.88577"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="stroke-width:0.26458332px"
y="53.51746"
x="112.88577"
id="tspan13812"
sodipodi:role="line">We acquired 500 doses!</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="108.70142"
y="114.62009"
id="text13818"><tspan
sodipodi:role="line"
id="tspan13816"
x="108.70142"
y="114.62009"
style="font-weight:bold;stroke-width:0.26458332px">???</tspan></text>
<text
id="text3109"
y="36.388439"
x="108.80019"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-weight:normal;font-size:4.93888903px;stroke-width:0.26458332px"
y="36.388439"
x="108.80019"
id="tspan3107"
sodipodi:role="line">T0: 5000</tspan></text>
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="ACID"
style="display:none">
<rect
ry="5.2916665"
y="67.923912"
x="61.174206"
height="39.498508"
width="54.42857"
id="rect14092"
style="opacity:1;vector-effect:none;fill:#f9f9f9;fill-opacity:0.97409327;fill-rule:nonzero;stroke:#000000;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.91191711;paint-order:fill markers stroke" />
<rect
style="opacity:1;vector-effect:none;fill:#f9f9f9;fill-opacity:0.97409327;fill-rule:nonzero;stroke:#000000;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.91191711;paint-order:fill markers stroke"
id="rect14090"
width="54.42857"
height="39.498508"
x="1.6973262"
y="21.886396"
ry="5.2916665" />
<path
inkscape:connector-curvature="0"
id="path14037"
d="m 56.193207,64.654408 h 4.913692"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 56.193207,110.69192 h 4.913692"
id="path14040"
inkscape:connector-curvature="0" />
<g
id="g14075"
transform="translate(0,-1.9988201)">
<g
id="g13993"
transform="translate(-43.663711,-25.06345)"
style="display:inline">
<path
inkscape:connector-curvature="0"
id="path13977"
d="M 97.820644,51.619712 75.828475,64.316897"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker1115)" />
<text
y="-108.69946"
x="-183.00148"
transform="rotate(180,91.874093,54.229103)"
id="text13983"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><textPath
id="textPath13981"
xlink:href="#path850"><tspan
id="tspan13979"
style="font-weight:bold;stroke-width:0.26458332px">R</tspan></textPath></text>
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker9668)"
d="M 75.056888,72.78638 97.049057,85.483565"
id="path13985"
inkscape:connector-curvature="0" />
<text
transform="translate(9.4212508,3.3408691)"
y="2.986012"
x="8.5186663"
id="text13991"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><textPath
id="textPath13989"
xlink:href="#path9498"><tspan
id="tspan13987"
style="font-weight:bold;stroke-width:0.26458332px">W</tspan></textPath></text>
</g>
<text
id="text14044"
y="29.833508"
x="3.4844763"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="stroke-width:0.26458332px"
y="29.833508"
x="3.4844763"
id="tspan14042"
sodipodi:role="line">AcqDoses(1000)</tspan></text>
</g>
<g
id="g14088"
transform="translate(0,-0.41130478)">
<g
id="g14011"
transform="translate(-43.663711,19.386549)"
style="display:inline">
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker12071)"
d="m 106.80689,51.619712 21.99218,12.697185"
id="path13995"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path13997"
d="M 129.57066,72.78638 107.57848,85.483565"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker9668)" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="text14003"
transform="rotate(-115.89278,108.31787,47.643413)"
x="-372.04132"
y="-217.15767"><textPath
xlink:href="#path850"
id="textPath14001"><tspan
style="font-weight:bold;stroke-width:0.26458332px"
id="tspan13999">R</tspan></textPath></text>
<text
transform="rotate(-58.856754,100.88692,38.870944)"
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="text14009"
x="46.604572"
y="6.6609678"><textPath
xlink:href="#path9498"
id="textPath14007"><tspan
style="font-weight:bold;stroke-width:0.26458332px"
id="tspan14005">W</tspan></textPath></text>
</g>
<text
id="text14044-8"
y="74.283508"
x="82.76487"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="stroke-width:0.26458332px"
y="74.283508"
x="82.76487"
id="tspan14042-8"
sodipodi:role="line">AcqDoses(500)</tspan></text>
</g>
<text
transform="translate(-43.663711,-16.067619)"
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="108.80019"
y="82.757004"
id="text832-6"><tspan
sodipodi:role="line"
id="tspan830-8"
x="108.80019"
y="82.757004"
style="font-weight:normal;font-size:4.93888903px;stroke-width:0.26458332px">T1: 6000</tspan></text>
<text
transform="translate(-43.663711,-16.067619)"
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="108.80019"
y="128.8701"
id="text832-3"><tspan
sodipodi:role="line"
id="tspan830-83"
x="108.80019"
y="128.8701"
style="font-weight:normal;font-size:4.93888903px;stroke-width:0.26458332px">T2: 6500</tspan></text>
<text
transform="translate(-43.663711,-16.067619)"
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="108.80019"
y="36.388439"
id="text3117"><tspan
sodipodi:role="line"
id="tspan3115"
x="108.80019"
y="36.388439"
style="font-weight:normal;font-size:4.93888903px;stroke-width:0.26458332px">T0: 5000</tspan></text>
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="BASE"
style="display:none">
<g
id="g1463"
transform="translate(0,-21.166667)">
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker1449)"
d="M 54.156933,47.722926 32.164764,60.420111"
id="path1369"
inkscape:connector-curvature="0" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="text1375"
transform="rotate(180,70.308075,52.117558)"
x="-279.42224"
y="-209.36409"><textPath
xlink:href="#path850"
id="textPath1373"><tspan
style="font-weight:bold;stroke-width:0.26458332px"
id="tspan1371">R</tspan></textPath></text>
</g>
<g
id="g1481"
transform="translate(0,-21.166667)">
<path
inkscape:connector-curvature="0"
id="path1377"
d="M 31.393177,68.889594 53.385346,81.586779"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker1467)" />
<text
transform="translate(-34.395836)"
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="text1383"
x="17.939917"
y="6.3268809"><textPath
xlink:href="#path9498"
id="textPath1381"><tspan
style="font-weight:bold;stroke-width:0.26458332px"
id="tspan1379">W</tspan></textPath></text>
</g>
<g
style="display:inline"
transform="translate(-43.663711,-25.063453)"
id="g1403">
<path
inkscape:connector-curvature="0"
id="path1387"
d="m 106.80689,51.619712 21.99218,12.697185"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker12071)" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker9668)"
d="M 129.57066,72.78638 107.57848,85.483565"
id="path1389"
inkscape:connector-curvature="0" />
<text
y="-217.15767"
x="-372.04132"
transform="rotate(-115.89278,108.31787,47.643413)"
id="text1395"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><textPath
id="textPath1393"
xlink:href="#path850"><tspan
id="tspan1391"
style="font-weight:bold;stroke-width:0.26458332px">R</tspan></textPath></text>
<text
y="6.6609678"
x="46.604572"
id="text1401"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"
transform="rotate(-58.856754,100.88692,38.870944)"><textPath
id="textPath1399"
xlink:href="#path9498"><tspan
id="tspan1397"
style="font-weight:bold;stroke-width:0.26458332px">W</tspan></textPath></text>
</g>
<path
inkscape:connector-curvature="0"
id="path1503"
d="m 56.193207,73.650233 h 4.913692"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<text
id="text1507"
y="75.354156"
x="64.607315"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-weight:normal;font-size:4.93888903px;stroke-width:0.26458332px"
y="75.354156"
x="64.607315"
id="tspan1505"
sodipodi:role="line">T1: [(T0, 6000),(T0, 5500)]</tspan></text>
<g
id="g1530"
transform="translate(0,38.100002)">
<path
inkscape:connector-curvature="0"
id="path1522"
d="M 54.156933,47.722926 32.164764,60.420111"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker1449)" />
<text
y="-209.36409"
x="-279.42224"
transform="rotate(180,70.308075,52.117558)"
id="text1528"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><textPath
id="textPath1526"
xlink:href="#path850"><tspan
id="tspan1524"
style="font-weight:bold;stroke-width:0.26458332px">R</tspan></textPath></text>
</g>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="8.5044641"
y="109.58665"
id="text1534"><tspan
sodipodi:role="line"
id="tspan1532"
x="8.5044641"
y="109.58665"
style="font-weight:normal;stroke-width:0.26458332px">Now solve this!</tspan><tspan
sodipodi:role="line"
x="8.5044641"
y="115.76025"
style="font-weight:normal;stroke-width:0.26458332px"
id="tspan3159">And write back!</tspan></text>
<text
id="text1538"
y="44.886383"
x="6.3156896"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="stroke-width:0.26458332px"
y="44.886383"
x="6.3156896"
id="tspan1536"
sodipodi:role="line">x + 1000</tspan></text>
<text
id="text1542"
y="44.886383"
x="93.178284"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="stroke-width:0.26458332px"
y="44.886383"
x="93.178284"
id="tspan1540"
sodipodi:role="line">x + 500</tspan></text>
<text
transform="translate(-43.663711,-16.067619)"
id="text3103"
y="36.388439"
x="108.80019"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-weight:normal;font-size:4.93888903px;stroke-width:0.26458332px"
y="36.388439"
x="108.80019"
id="tspan3101"
sodipodi:role="line">T0: (⊥, 5000)</tspan></text>
</g>
<g
inkscape:groupmode="layer"
id="layer5"
inkscape:label="CRDT"
style="display:inline">
<g
style="display:inline"
transform="translate(0,-21.166667)"
id="g2920">
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker1467)"
d="M 31.393177,68.889594 53.385346,81.586779"
id="path2912"
inkscape:connector-curvature="0" />
<text
y="6.3268809"
x="17.939917"
id="text2918"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"
transform="translate(-34.395836)"><textPath
id="textPath2916"
xlink:href="#path9498"><tspan
id="tspan2914"
style="font-weight:bold;stroke-width:0.26458332px">W</tspan></textPath></text>
</g>
<g
id="g3013">
<path
inkscape:connector-curvature="0"
id="path2924"
d="M 85.906949,47.722927 63.914769,60.420112"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker2999)" />
<text
transform="rotate(-58.856754,57.228059,65.935492)"
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="text2936"
x="7.1585193"
y="48.970455"><textPath
xlink:href="#path9498"
id="textPath2934"><tspan
style="font-weight:bold;stroke-width:0.26458332px"
id="tspan2932">W</tspan></textPath></text>
</g>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="15.648211"
y="44.007793"
id="text2976"><tspan
sodipodi:role="line"
id="tspan2974"
x="15.648211"
y="44.007793"
style="font-weight:normal;stroke-width:0.26458332px">incr(1000)</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="82.927979"
y="44.007793"
id="text2980"><tspan
sodipodi:role="line"
id="tspan2978"
x="82.927979"
y="44.007793"
style="font-weight:normal;stroke-width:0.26458332px">incr(500)</tspan></text>
<path
inkscape:connector-curvature="0"
id="path2982"
d="m 56.193207,72.591896 h 4.913692"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<text
id="text2986"
y="74.295815"
x="65.136482"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-weight:normal;font-size:4.93888903px;stroke-width:0.26458332px"
y="74.295815"
x="65.136482"
sodipodi:role="line"
id="tspan2995">T1: (5000, </tspan><tspan
style="font-weight:normal;font-size:4.93888903px;stroke-width:0.26458332px"
y="79.777985"
x="65.136482"
sodipodi:role="line"
id="tspan1204"> {(a, 1000), (b, 500)})</tspan></text>
<g
style="display:inline"
transform="translate(0,38.100003)"
id="g3042">
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker1449)"
d="M 54.156933,47.722926 32.164764,60.420111"
id="path3034"
inkscape:connector-curvature="0" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="text3040"
transform="rotate(180,70.308075,52.117558)"
x="-279.42224"
y="-209.36409"><textPath
xlink:href="#path850"
id="textPath3038"><tspan
style="font-weight:bold;stroke-width:0.26458332px"
id="tspan3036">R</tspan></textPath></text>
</g>
<text
id="text3051"
y="102.21611"
x="17.764879"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-weight:normal;stroke-width:0.26458332px"
y="102.21611"
x="17.764879"
id="tspan3049"
sodipodi:role="line">6500</tspan></text>
<text
transform="translate(-43.663711,-16.067619)"
id="text3097"
y="36.388439"
x="108.80019"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-weight:normal;font-size:4.93888903px;stroke-width:0.26458332px"
y="36.388439"
x="108.80019"
id="tspan3095"
sodipodi:role="line">T0: (5000, ∅)</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 52 KiB

@@ -0,0 +1,227 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="82.010674mm"
height="17.561125mm"
viewBox="0 0 82.010674 17.561125"
version="1.1"
id="svg1579"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="crdt.svg">
<defs
id="defs1573">
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker2645"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="Arrow2Mend">
<path
transform="scale(-0.6)"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
id="path2643"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker2605"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="Arrow2Mstart">
<path
transform="scale(0.6)"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
id="path2603"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="Arrow2Mend"
orient="auto"
refY="0"
refX="0"
id="Arrow2Mend"
style="overflow:visible"
inkscape:isstock="true"
inkscape:collect="always">
<path
id="path2206"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
transform="scale(-0.6)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="Arrow2Mstart"
orient="auto"
refY="0"
refX="0"
id="Arrow2Mstart"
style="overflow:visible"
inkscape:isstock="true"
inkscape:collect="always">
<path
id="path2203"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
transform="scale(0.6)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="Arrow2Lstart"
orient="auto"
refY="0"
refX="0"
id="Arrow2Lstart"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path2197"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
transform="matrix(1.1,0,0,1.1,1.1,0)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="Arrow1Sstart"
orient="auto"
refY="0"
refX="0"
id="Arrow1Sstart"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path2191"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
transform="matrix(0.2,0,0,0.2,1.2,0)"
inkscape:connector-curvature="0" />
</marker>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="2.8"
inkscape:cx="148.82928"
inkscape:cy="-32.722297"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:snap-bbox="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1920"
inkscape:window-height="1055"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata1576">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-21.438257,-19.857903)">
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#Arrow2Mstart);marker-end:url(#Arrow2Mend)"
d="M 44.979166,28.87314 H 58.822544"
id="path2177"
inkscape:connector-curvature="0" />
<g
id="g2153">
<rect
rx="0"
ry="3.8793426"
y="19.990194"
x="21.570549"
height="17.296541"
width="23.537758"
id="rect2124"
style="opacity:1;vector-effect:none;fill:#f9f9f9;fill-opacity:0.97409327;fill-rule:nonzero;stroke:#000000;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.91191711;paint-order:fill markers stroke" />
<text
id="text2132"
y="29.799105"
x="29.366098"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-weight:normal;stroke-width:0.26458332px"
y="29.799105"
x="29.366098"
id="tspan2130"
sodipodi:role="line">App</tspan></text>
</g>
<rect
rx="0"
ry="3.8793426"
y="19.990194"
x="58.612213"
height="17.296541"
width="23.537758"
id="rect2155"
style="opacity:1;vector-effect:none;fill:#f9f9f9;fill-opacity:0.97409327;fill-rule:nonzero;stroke:#000000;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.91191711;paint-order:fill markers stroke" />
<text
id="text2159"
y="30.367077"
x="63.842014"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-weight:normal;stroke-width:0.26458332px"
y="30.367077"
x="63.842014"
id="tspan2157"
sodipodi:role="line">CRDT</tspan></text>
<g
id="g4137"
transform="translate(-15.875)">
<rect
rx="0"
ry="3.8793426"
y="19.990194"
x="95.653885"
height="17.296541"
width="23.537758"
id="rect2163"
style="opacity:1;vector-effect:none;fill:#f9f9f9;fill-opacity:0.97409327;fill-rule:nonzero;stroke:#000000;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.91191711;paint-order:fill markers stroke" />
<text
id="text2167"
y="30.367077"
x="96.969627"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-weight:normal;stroke-width:0.26458332px"
y="30.367077"
x="96.969627"
id="tspan2165"
sodipodi:role="line">Datastore</tspan></text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.4 KiB

@@ -0,0 +1,147 @@
\section{Introducing Garage}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}{Brought to you by the Deuxfleurs association}
\begin{block}{\textbf{deuxfleurs.fr} -- a libre hosting association with a vision}
``Shifting the current structure of the Internet from a world of a few very large service providers, to a world where services are hosted by a variety of smaller organisations.''
\end{block}
\begin{block}{Our goals}
\begin{itemize}
\item To propose performant \& reliable libre services for the masses
\item To host and administer our infrastructure ourselves
\item To allow members to contribute storage/compute nodes
\item Resilience: for availability \& the sysadmins' sleep
\item Conceptual simplicity to ease onboarding \& demistify hosting
\end{itemize}
\end{block}
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}{The lacking state of the practice}
\begin{block}{Object storage fitted our needs}
\begin{itemize}
\item Distributed by design
\item Objects are replicated
\item Conceptually simple
\end{itemize}
\end{block}
\vfill
\begin{block}{Existing object stores did not}
\begin{itemize}
\item Too specific / complex
\item Resource hungry
\item Hidden constraints
\end{itemize}
\end{block}
\vfill
We developed Garage, an object store with minimal functionality.
It works, and serves our static sites and media.
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}{Introducing Garage}
\centering
\url{garagehq.deuxfleurs.fr}
\url{git.deuxfleurs.fr/Deuxfleurs/garage}
\includegraphics[width=.4\columnwidth]{figures/garage_distributed.png}
\vfill
\raggedright
\begin{itemize}
\item Distributed data store
\item Based on DynamoDB object store (P2P!)
\item Modular data types/protocols with CRDTs:
\begin{itemize}
\item Done: objects (media, static sites, backups...) via S3 API
\item To do: e-mails via IMAP protocol, and more
\end{itemize}
\end{itemize}
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}[t]{The \textbf{RING}}
\centering
\fullcite{decandia_dynamo:_2007}
\vspace{3ex}
\only<1>{\includegraphics[width=.5\columnwidth]{figures/c1.pdf}}%
\only<2>{\includegraphics[width=.5\columnwidth]{figures/c2.pdf}}%
\only<3>{\includegraphics[width=.5\columnwidth]{figures/c3.pdf}}%
\only<4>{\includegraphics[width=.5\columnwidth]{figures/c4.pdf}}%
\vspace{5ex}
%\raggedright
\only<1>{Each node is assigned a unique ID on the circular address space.}%
\only<2-3>{When a new object is added to the store...}%
\only<3>{\\ It is assigned a unique ID (its \emph{key}) on the address space.}%
\only<4>{The $R$ nodes after the object are in charge of replicating it.}%
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}{Distributed metadata}
\centering
\includegraphics[width=.8\columnwidth]{figures/garage_tables.pdf}
\vfill
The objects, versions and blocks are all stored in the ring.
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}{Written in Rust}
\begin{columns}
\column{.65\columnwidth}
Entirely written in Rust!
\column{.35\columnwidth}
\centering
\includegraphics[width=.85\columnwidth]{figures/rustacean-flat-happy.png}
\end{columns}
\vfill
\begin{columns}
\column[t]{.6\columnwidth}
\textbf{Pros:}
\begin{itemize}
\item Compiled and fast
\item Features prevent usual mistakes:
strongly typed, immutable by default, ownership instead of GC...
\item Best of several paradigms:
imperative, OO, functional
\item Good libraries for network programmings:
serialization, http, async/await...
\end{itemize}
\column[t]{.4\columnwidth}
\textbf{Cons}:
\begin{itemize}
\item Steep learning curve
\item Long compilation times
\item Compiler rage
\end{itemize}
\end{columns}
\end{frame}
@@ -0,0 +1,134 @@
%% Imports %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\usepackage{graphicx}
\usepackage[utf8]{inputenc}
\usepackage{csquotes} % Elegant quotes (must be loaded before babel)
\usepackage[english]{babel}
\usepackage[T1]{fontenc}
% \usepackage{amsfonts}
% \usepackage{amssymb}
%\usepackage{lmodern}
\usepackage{iwona}
\usepackage{color}
\usepackage{xspace}
\usepackage{amsmath}
\usepackage{hanging}
\usepackage{listings}
\lstset{basicstyle=\scriptsize}
\usepackage{tikz}
\usepackage{tikzsymbols} % For emojis
%\usepackage{setspace} % Activation disables footnoes
\usepackage{perpage} % Reset footnote counter every page
\MakePerPage{footnote}
\usepackage{nameref} % For printing the sections' name
\usepackage{hyperref}
%% Biblio %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\usepackage[style=alphabetic,giveninits=true,sorting=none,hyperref,backend=biber,maxnames=3]{biblatex}
\addbibresource{bibliography.bib}
%% Templating %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\usepackage{pgf}
\usepackage{xifthen, tikz}
\usetheme{default}
\makeatletter
%%% colors
\definecolor{bleuroi}{RGB}{0, 8, 88}
\definecolor{vertemeraude}{RGB}{0, 64, 44}
\definecolor{lightgray}{RGB}{245,245,245}
\definecolor{encre_de_chine}{RGB}{9,36,53}
\definecolor{im_lost_in_your_eyes}{RGB}{110,138,159}
\definecolor{metallic_grey}{RGB}{209,219,221}
\definecolor{metallic_white}{RGB}{229,239,241}
\definecolor{boss}{RGB}{229,72,27}
\definecolor{toon_eyes}{RGB}{3,4,6}
\setbeamercolor{frametitle}{bg=bleuroi,fg=white}
\setbeamercolor*{normal text}{fg=toon_eyes,bg=white}
\setbeamercolor*{block title}{fg=white,bg=vertemeraude}
\setbeamercolor*{block body}{fg=toon_eyes,bg=vertemeraude!5}
\setbeamercolor{alerted text}{fg=vertemeraude}
\setbeamercolor*{example text}{fg=toon_eyes,bg=white}
\setbeamercolor*{structure}{fg=bleuroi,bg=white}
% sectionnavigation:
\setbeamercolor*{section in head/foot}{fg=bleuroi,bg=white}
% \let\oldtextbf\textbf
% \renewcommand{\textbf}[1]{\textcolor{bleuroi}{\oldtextbf{#1}}}
%%% Rounded boxes with no shading
\pgfdeclareverticalshading[lower.bg,upper.bg]{bmb@transition}{200cm}{%
color(0pt)=(upper.bg); color(2pt)=(upper.bg); color(4pt)=(upper.bg)}
\setbeamertemplate{blocks}[rounded][shadow=false]
%%% Who on Earth uses navigation bars?
\setbeamertemplate{navigation symbols}{}
%%% Headline with sections
\setbeamertemplate{headline}
{%
\begin{beamercolorbox}[wd=\paperwidth,dp=.5ex,ht=2ex]{section in head/foot}
\insertsectionnavigationhorizontal{\paperwidth}{\hskip0pt plus1fill}{\hskip0pt plus1fill}\par % Centered
%\insertsectionnavigationhorizontal{\paperwidth}{}{\hfill\hfill} % Left aligned
\end{beamercolorbox}%
}
% Set the colors of the section bar
\usesectionheadtemplate
{\colorbox{fg}{\color{bg} \insertsectionhead}}
{\color{fg!40!bg} \insertsectionhead}
% \newlength\lpg@linewd
% \setbox0=\hbox{\strut}
% \newlength\strutht \strutht\ht0
% \newlength\strutdp \strutdp\dp0
\setbeamertemplate{frametitle}
{
\begin{beamercolorbox}[wd=\paperwidth,dp=1ex,ht=2.6ex,leftskip=.7cm,rightskip=.7cm]{frametitle}
%\vskip-1ex
%[wd=\lpg@linewd, ht=\strutht, dp=\strutdp]{frametitle}
%\vskip-.8ex\hskip0.7cm
%\vskip-.8ex
\usebeamerfont{frametitle}\strut\insertframetitle
\ifx\insertframesubtitle\@empty
\else
\hfill \usebeamerfont{framesubtitle}\strut\insertframesubtitle
\fi%
\end{beamercolorbox}
}
%%% Page numbering in footline
\setbeamertemplate{footline}{
\hbox{\begin{beamercolorbox}[wd=1\paperwidth,ht=2.5ex,dp=1ex,right]{framenumber in head/foot}%
\usebeamerfont{framenumber in head/foot}%
%\insertframenumber\space/\space\inserttotalframenumber\hspace{0.3em}
\insertframenumber\hspace{0.5em}
\end{beamercolorbox}}}
%%% Reduce foot citation size
\renewcommand{\footnotesize}{\tiny}
%%% Display footnotes (not working, still no footnotes)
% \setbeamertemplate{footnote}{%
% \hangpara{2em}{1}%
% \makebox[2em][l]{\insertfootnotemark}\footnotesize\insertfootnotetext\par%
% }
\makeatother
%% Commentaires %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\newcommand{\annote}[3]{{
\colorbox{#3}{\bfseries\sffamily\footnotesize\textcolor{white}{#2}}
\color{#3}
$\blacktriangleright$\textit{#1}$\blacktriangleleft$}
}
%% Uncomment for final version (removes notes)
%\renewcommand{\annote}[3]{}
\newcommand{\todo}[1]{\annote{#1}{TODO}{green}}
@@ -0,0 +1,99 @@
\section{Introduction}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}{A very casual motivation}
\begin{center}
\Large
I want to host \textbf{resilient web services} with \textbf{acceptable performance} on commodity hardware behind \textbf{household networks}.
\end{center}
\vfill
\begin{block}{Keywords}
\begin{columns}
\column{.5\columnwidth}
\begin{itemize}
\item Decentralised networks
\item Edge computing
\end{itemize}
\column{.5\columnwidth}
\begin{itemize}
\item Distributed storage
\item Privacy
\end{itemize}
\end{columns}
\end{block}
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}{Context}
\textbf{Resilience}: Ability to recover quickly from failures and changes.
\vspace{1ex}
Only achievable through distribution of the hosted applications across several physical locations.
\vfill
\begin{block}{Application = \textbf{computations} on \textbf{data}}
\begin{itemize}
\item \textbf{Computation}: Stateless; easy to distribute \& orchestrate.
% where it is performed does not matter as long as the application's state is accessible R/W
%Computation units are "easy" to distribute and orchestrate.
\item \textbf{Data}: Stateful; hard to distribute \& full of trade-offs.
\end{itemize}
\end{block}
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}{Concurrent writes example}{How to lose vaccines}
\centering
\includegraphics[width=.5\columnwidth]{figures/conflict_problem.pdf}
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}{The problem}
% \textbf{Path dependency}: existing data stores are built for data centres.
% $\implies$ They assume good inter-node connectivity.
% \vfill
\begin{center}
\Large
Can we design an available data store tailored for adverse network conditions?
\end{center}
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Maybe more framing of the context. What kind of data storage? Object vs Block vs what?
% \begin{frame}{``Stateless'', ``serverless'', and the elephant in the room}
% It seems easy to deploy \& administer web services nowadays ...
% Because the inherent complexity is shadowed by proprietary ``cloud'' solutions.
% The IT crowd can gloss over ``statelessness'' \emph{ad nauseam} ...
% But storing \emph{state} remains an open research problem.
% Data storage is either:
% \begin{itemize}
% \item A single point of failure;
% \item Delegated to proprietary solutions;
% \item Pain.
% \end{itemize}
% Today, we will review networked storage's history, and discuss open research questions.
% \end{frame}
@@ -0,0 +1,70 @@
\documentclass[compress]{beamer}
\input{header.tex}
%% Metadata %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\title{Distributed object storage is centralised}
\subtitle{A quest for autonomy in the modern hosting ecology}
\author{Adrien Luxey}
\date{Wednesday, 28th April, 2021}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Begin document %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{document}
%% Title page %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
{
\setbeamertemplate{footline}{}
\setbeamertemplate{headline}{}
\begin{frame}
\titlepage
\end{frame}
}
%% Contents %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\input{introduction.tex}
\input{sota.tex}
\input{escaping_the_cloud.tex}
\input{garage.tex}
\input{conclusion.tex}
%% Bibliography %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% \begin{frame}
% \large
% \raggedright
% Références
% \printbibliography
% \end{frame}
%% Ending %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
{
\setbeamertemplate{footline}{}
\setbeamertemplate{headline}{}
\begin{frame}
\centering
\vspace{1cm}
\Large Thank you for your attention.
\vspace{2cm}
\large Now let's chat!
\vspace{1cm}
\raggedleft
\includegraphics[width=.2\textwidth]{figures/m_proxy_gray.pdf}
\end{frame}
}
%% Appendices %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% \appendix
% \input{sprinkler_appendix.tex}
% \input{cascade_appendix.tex}
% \input{spores_appendix.tex}
\end{document}
+323
View File
@@ -0,0 +1,323 @@
\section{State of the art}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}{The CAP theorem}{Consistency vs. Availability}
\begin{block}{Eric Brewer's theorem}
``A shared-state system can have \textbf{at most two} of the following properties at any given time:
\begin{itemize}
\item \textbf{C}onsistency
\item \textbf{A}vailability
\item \textbf{P}artition tolerance''
\end{itemize}
\end{block}
\begin{center}
\Large
Under network partitions, a distributed data store has to sacrifice either availability or consistency.
\end{center}
\vfill
\begin{itemize}
\item \textbf{Consistency-first}: Abort incoming queries;
\item \textbf{Availability-first}: Return possibly stale data.
\end{itemize}
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}{Consistency-first: the ACID model}{Consistency vs. Availability}
\textbf{Transaction}: unit of work within an ACID data store.
%Comprises multiple operations.
%E.g. bank transfer.
%E.g. a bank transfer from A to B is a transaction involving two operations: withdraw money from A & credit B with the same money amount.
\vfill
\begin{itemize}
\item \textbf{\underline{A}tomicity}: Transactions either complete entirely or fail.
No transaction ever seen as in-progress.
\item \textbf{\underline{C}onsistency}: Transactions always generate a valid state.
The database maintains its invariants across transactions.
\item \textbf{\underline{I}solation}: Concurrent transactions are seen as sequential.
Transactions are serializable, or sequentially consistent.
\item \textbf{\underline{D}urability}: Committed transactions are never forgotten.
\end{itemize}
\vfill\centering
Reads are fast, writes are slow.
\vfill\raggedright
Example: relational databases.
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}[fragile]{Concurrent writes in ACID}{Consistency vs. Availability}
\begin{columns}
\column{.5\columnwidth}
\begin{block}{}
\begin{lstlisting}
transaction AcqDoses(y):
x <- SELECT #vaccines;
UPDATE #vaccines = (x + y);
\end{lstlisting}
\end{block}
\vspace{5ex}
Supports compound operations.
\column{.5\columnwidth}
\centering
\includegraphics[width=\columnwidth]{figures/conflict_acid.pdf}
\end{columns}
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}{Availability-first: the BASE model}{Consistency vs. Availability}
Some apps prefer availability, e.g. Amazon products' reviews.
\vfill
The BASE model trades Consistency \& Isolation for Availability.
%Some applications do not care about strong consistency and prefer being highly available (e.g. Amazon's product reviews).
%In order to achieve higher availability, the BASE model relaxes consistency constraints of the ACID model: "eventual consistency".
\vfill
\begin{itemize}
\item \textbf{\underline{B}asic \underline{A}vailability}:
The data store thrives to be available.
\item \textbf{\underline{S}oft-state}:
Replicas can disagree on the valid state.
\item \textbf{\underline{E}ventual consistency}:
In the absence of write queries,
the data store will eventually converge to a single valid state.
\end{itemize}
\vfill\centering
Writes are fast, reads are slow.
\vfill\raggedright
Examples: key-value \& object stores.
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}{Concurrent writes in BASE}{Consistency vs. Availability}
\begin{columns}
\column{.5\columnwidth}
\begin{block}{Object}
\begin{itemize}
\item Unique key
\item Arbitrary value
\item Metadata
\end{itemize}
\end{block}
\vspace{5ex}
Conflict resolution = client's job!
\vspace{5ex}
No compound operations.
\column{.5\columnwidth}
\centering
\includegraphics[width=\columnwidth]{figures/conflict_base.pdf}
\end{columns}
% KV storage is another example, distinction is minor here
% Object = unique key, arbitrary value, metadata.
% Object storage only provides semantics to investigate causal order of queries *for individual objects*. No compound operations, no transactions.
% Much easier to distribute, and "scale-out".
% Write is fast, read is slow (gotta collect all object versions).
% \todo{vaccines example with BASE model}
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}{Strong Eventual Consistency w/ CRDTs}{Consistency vs. Availability}
\centering\small
\fullcite{defago_conflict-free_2011}
\vfill\raggedright\normalsize
\begin{block}{Strong Eventual Consistency (SEC)}
\begin{itemize}
\item CRDTs specify distributed operations
\item Conflicts will be solved according to specification
\item Proven \& bound eventual convergence
\end{itemize}
\end{block}
\vfill\centering
\includegraphics[width=.5\columnwidth]{figures/crdt.pdf}
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}[fragile]{Concurrent writes with CRDTs}{Consistency vs. Availability}
\begin{columns}
\column{.5\columnwidth}
\begin{block}{}
\begin{lstlisting}
CRDT Counter(x0):
history = {}
op. incr(y):
history U= {(UUID(), y)}
op. decr(y):
history U= {(UUID(), -y)}
op. read():
x = x0
for (_, y) in history:
x += y
return x
\end{lstlisting}
\end{block}
\vspace{2ex}
Operations commute?
$\implies$ screw total order!
\column{.5\columnwidth}
\centering
\includegraphics[width=\columnwidth]{figures/conflict_crdt.pdf}
\end{columns}
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}{A complex CRDT: the DAG}{Consistency vs. Availability}
\centering
\only<1>{\includegraphics[height=\textheight]{figures/dag_crdt.png}}%
\only<2>{
Just to say I swept a lot under the rug.
\vfill
For details, go read:
\fullcite{defago_conflict-free_2011}
\vfill
For an implementation, check \textbf{AntidoteDB}.
}
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\begin{frame}{State of the practice}{Path dependency to the ``cloud''}
\begin{block}{The BASE model is fashionable because}
\centering
``\emph{High-performance} object storage for \emph{AI analytics} with PBs of \emph{IoT data streams} at the \emph{edge}, using \emph{5G}.''
% \begin{itemize}
% \item Highest performance
% \item IoT data streams are inherently distributed
% \end{itemize}
\end{block}
\vfill\centering
\includegraphics[width=.9\columnwidth]{figures/minio_edge.png}
\vfill\raggedright
%\begin{block}{}
\begin{itemize}
\item Always backed by cloud: high performance network links.
\item Edge nodes always seen as clients or data sources, not peers.
\end{itemize}
%\end{block}
% There is \textbf{always a central cloud cluster} in these use-cases.
% Hidden constraint: \textbf{high performance inter-node connectivity}.
\end{frame}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% \begin{frame}{A brief history of storage}
% We keep it short because we'll follow chronological order in the next section too.
% \end{frame}
% \begin{frame}{In the beginning, there were \emph{monoliths}}
% \includegraphics[width=.5\columnwidth]{figures/stonehenge.jpg}
% Web applications used to be monolithic:
% \begin{itemize}
% \item One or two servers;
% \item Availability was not an obsession;
% \item Latency was acceptable.
% \end{itemize}
% Relational databases were queens.
% \end{frame}
% \begin{frame}{Then came \emph{expectations}}
% Then, the whole world went online, and suddenly: expectations!
% \begin{itemize}
% \item ``Milliseconds matter.'' (Algolia slogan)
% \item Critical networked services (healthcare, logistics) need 100\% availability
% \end{itemize}
% $\implies$ Microservices \& horizontal scalability.
% \todo{Develop on the `herd not sheep' paradigm a bit.}
% \end{frame}
% \begin{frame}{Distributing state/storage: the remaining unknown}
% The microservices orchestration game works well for \emph{stateless} services.
% However, any application requires \emph{state}, persistent data.
% And this is tough. As we will now see.
% (Not that it's not well studied: distributed storage has always been fashionable.)
% \end{frame}
Binary file not shown.
Executable
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
BIN=target/release/garage
DOCKER=lxpz/garage_amd64
TAG=$1
if [ -z "$1" ]; then
echo "Usage: $0 <tag>"
exit 1
fi
RUSTFLAGS="-C link-arg=-fuse-ld=lld -C target-cpu=x86-64 -C target-feature=+sse2" cargo build --release --no-default-features
cp $BIN $BIN.stripped
strip $BIN.stripped
docker pull archlinux:latest
docker build -t $DOCKER:$TAG .
docker push $DOCKER:$TAG
docker tag $DOCKER:$TAG $DOCKER:latest
docker push $DOCKER:latest
+3 -2
View File
@@ -1,6 +1,7 @@
FROM rust:buster FROM rust:buster
RUN apt-get update && \ RUN apt-get update && \
apt-get install --yes libsodium-dev awscli python-pip && \ apt-get install --yes libsodium-dev awscli python-pip wget rclone openssl socat && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
RUN rustup component add rustfmt RUN wget https://dl.min.io/client/mc/release/linux-amd64/mc -O /usr/local/bin/mc && chmod +x /usr/local/bin/mc
RUN rustup component add rustfmt clippy
RUN pip install s3cmd RUN pip install s3cmd
+1
View File
@@ -3,5 +3,6 @@
set -ex set -ex
killall -9 garage || echo "garage is not running" killall -9 garage || echo "garage is not running"
killall -9 socat || echo "socat is not running"
rm -rf /tmp/garage* rm -rf /tmp/garage*
rm -rf /tmp/config.*.toml rm -rf /tmp/config.*.toml
+16 -3
View File
@@ -31,9 +31,7 @@ bootstrap_peers = [
"127.0.0.1:3903" "127.0.0.1:3903"
] ]
max_concurrent_rpc_requests = 12 max_concurrent_rpc_requests = 12
data_replication_factor = 3 replication_mode = "3"
meta_replication_factor = 3
meta_epidemic_fanout = 3
[s3_api] [s3_api]
api_bind_addr = "0.0.0.0:$((3910+$count))" # the S3 API port, HTTP without TLS. Add a reverse proxy for the TLS part. api_bind_addr = "0.0.0.0:$((3910+$count))" # the S3 API port, HTTP without TLS. Add a reverse proxy for the TLS part.
@@ -47,6 +45,21 @@ EOF
echo -en "$LABEL configuration written to $CONF_PATH\n" echo -en "$LABEL configuration written to $CONF_PATH\n"
if [ -z "$SKIP_HTTPS" ]; then
echo -en "$LABEL Starting dummy HTTPS reverse proxy\n"
mkdir -p /tmp/garagessl
openssl req \
-new \
-x509 \
-keyout /tmp/garagessl/test.key \
-out /tmp/garagessl/test.crt \
-nodes \
-subj "/C=XX/ST=XX/L=XX/O=XX/OU=XX/CN=localhost/emailAddress=X@X.XX" \
-addext "subjectAltName = DNS:localhost, IP:127.0.0.1"
cat /tmp/garagessl/test.key /tmp/garagessl/test.crt > /tmp/garagessl/test.pem
socat openssl-listen:4443,reuseaddr,fork,cert=/tmp/garagessl/test.pem,verify=0 tcp4-connect:localhost:3911 &
fi
(garage server -c /tmp/config.$count.toml 2>&1|while read r; do echo -en "$LABEL $r\n"; done) & (garage server -c /tmp/config.$count.toml 2>&1|while read r; do echo -en "$LABEL $r\n"; done) &
done done
+1 -1
View File
@@ -18,6 +18,6 @@ garage status \
| grep UNCONFIGURED \ | grep UNCONFIGURED \
| grep -Po '^[0-9a-f]+' \ | grep -Po '^[0-9a-f]+' \
| while read id; do | while read id; do
garage node configure -d dc1 -c 1 $id garage node configure -z dc1 -c 1 $id
done done
+3 -8
View File
@@ -1,14 +1,9 @@
#!/bin/bash #!/bin/bash
SCRIPT_FOLDER="`dirname \"${BASH_SOURCE[0]}\"`"
REPO_FOLDER="${SCRIPT_FOLDER}/../"
GARAGE_DEBUG="${REPO_FOLDER}/target/debug/"
GARAGE_RELEASE="${REPO_FOLDER}/target/release/"
PATH="${GARAGE_DEBUG}:${GARAGE_RELEASE}:$PATH"
export AWS_ACCESS_KEY_ID=`cat /tmp/garage.s3 |cut -d' ' -f1` export AWS_ACCESS_KEY_ID=`cat /tmp/garage.s3 |cut -d' ' -f1`
export AWS_SECRET_ACCESS_KEY=`cat /tmp/garage.s3 |cut -d' ' -f2` export AWS_SECRET_ACCESS_KEY=`cat /tmp/garage.s3 |cut -d' ' -f2`
export AWS_DEFAULT_REGION='garage' export AWS_DEFAULT_REGION='garage'
alias awsgrg="aws s3 \ function aws { command aws --endpoint-url http://127.0.0.1:3911 $@ ; }
--endpoint-url http://127.0.0.1:3911"
aws --version
+52
View File
@@ -0,0 +1,52 @@
mkdir -p /tmp/garage.cyberduck.home/.duck/profiles
DUCK_ACCESS_KEY=`cat /tmp/garage.s3 |cut -d' ' -f1`
DUCK_SECRET_KEY=`cat /tmp/garage.s3 |cut -d' ' -f2`
cat > /tmp/garage.cyberduck.home/.duck/credentials <<EOF
https\://$DUCK_ACCESS_KEY@127.0.0.1\:4443=$DUCK_SECRET_KEY
EOF
cat > /tmp/garage.cyberduck.home/.duck/profiles/garage.cyberduckprofile <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Protocol</key>
<string>s3</string>
<key>Vendor</key>
<string>garage</string>
<key>Scheme</key>
<string>https</string>
<key>Description</key>
<string>GarageS3</string>
<key>Default Hostname</key>
<string>127.0.0.1</string>
<key>Default Port</key>
<string>4443</string>
<key>Hostname Configurable</key>
<false/>
<key>Port Configurable</key>
<false/>
<key>Username Configurable</key>
<true/>
<key>Username Placeholder</key>
<string>Access Key ID (GK...)</string>
<key>Password Placeholder</key>
<string>Secret Key</string>
<key>Properties</key>
<array>
<string>s3service.disable-dns-buckets=true</string>
</array>
<key>Region</key>
<string>garage</string>
<key>Regions</key>
<array>
<string>garage</string>
</array>
</dict>
</plist>
EOF
function duck { HOME=/tmp/garage.cyberduck.home/ command duck --username $DUCK_ACCESS_KEY $@ ; }
+22
View File
@@ -0,0 +1,22 @@
ACCESS_KEY=`cat /tmp/garage.s3 |cut -d' ' -f1`
SECRET_KEY=`cat /tmp/garage.s3 |cut -d' ' -f2`
mkdir -p /tmp/garage.mc/certs/CAs
cat > /tmp/garage.mc/config.json <<EOF
{
"version": "10",
"aliases": {
"garage": {
"url": "https://localhost:4443",
"accessKey": "$ACCESS_KEY",
"secretKey": "$SECRET_KEY",
"api": "S3v4",
"path": "auto"
}
}
}
EOF
function mc { command mc --insecure --config-dir /tmp/garage.mc $@ ; }
mc --version
+21
View File
@@ -0,0 +1,21 @@
ACCESS_KEY=`cat /tmp/garage.s3 |cut -d' ' -f1`
SECRET_KEY=`cat /tmp/garage.s3 |cut -d' ' -f2`
cat > /tmp/garage.rclone.conf <<EOF
[garage]
type = s3
provider = Other
env_auth = false
access_key_id = $ACCESS_KEY
secret_access_key = $SECRET_KEY
endpoint = http://127.0.0.1:3911
bucket_acl = private
force_path_style = true
region = garage
no_check_bucket = true
EOF
# It seems that region is mandatory as rclone does not support redirection
function rclone { command rclone --config /tmp/garage.rclone.conf $@ ; }
rclone --version
+11 -15
View File
@@ -1,19 +1,15 @@
#!/bin/bash
SCRIPT_FOLDER="`dirname \"${BASH_SOURCE[0]}\"`"
REPO_FOLDER="${SCRIPT_FOLDER}/../"
GARAGE_DEBUG="${REPO_FOLDER}/target/debug/"
GARAGE_RELEASE="${REPO_FOLDER}/target/release/"
PATH="${GARAGE_DEBUG}:${GARAGE_RELEASE}:$PATH"
ACCESS_KEY=`cat /tmp/garage.s3 |cut -d' ' -f1` ACCESS_KEY=`cat /tmp/garage.s3 |cut -d' ' -f1`
SECRET_KEY=`cat /tmp/garage.s3 |cut -d' ' -f2` SECRET_KEY=`cat /tmp/garage.s3 |cut -d' ' -f2`
alias s3grg="s3cmd \ cat > /tmp/garage.s3cmd.cfg <<EOF
--host 127.0.0.1:3911 \ [default]
--host-bucket 127.0.0.1:3911 \ access_key = $ACCESS_KEY
--access_key=$ACCESS_KEY \ secret_key = $SECRET_KEY
--secret_key=$SECRET_KEY \ host_base = 127.0.0.1:3911
--region=garage \ host_bucket = 127.0.0.1:3911
--no-ssl" use_https = False
EOF
function s3cmd { command s3cmd --config=/tmp/garage.s3cmd.cfg $@ ; }
s3cmd --version
python3 --version
+98 -46
View File
@@ -1,22 +1,26 @@
#!/bin/bash #!/bin/bash
set -ex set -ex
shopt -s expand_aliases
export LC_ALL=C.UTF-8 export LC_ALL=C.UTF-8
export LANG=C.UTF-8 export LANG=C.UTF-8
SCRIPT_FOLDER="`dirname \"$0\"`" SCRIPT_FOLDER="`dirname \"$0\"`"
REPO_FOLDER="${SCRIPT_FOLDER}/../" REPO_FOLDER="${SCRIPT_FOLDER}/../"
GARAGE_DEBUG="${REPO_FOLDER}/target/debug/"
GARAGE_RELEASE="${REPO_FOLDER}/target/release/"
PATH="${GARAGE_DEBUG}:${GARAGE_RELEASE}:$PATH"
echo "setup" # @FIXME Duck is not ready for testing, we have a bug
SKIP_DUCK=1
echo "⏳ Setup"
cargo build cargo build
${SCRIPT_FOLDER}/dev-clean.sh ${SCRIPT_FOLDER}/dev-clean.sh
${SCRIPT_FOLDER}/dev-cluster.sh > /tmp/garage.log 2>&1 & ${SCRIPT_FOLDER}/dev-cluster.sh > /tmp/garage.log 2>&1 &
${SCRIPT_FOLDER}/dev-configure.sh ${SCRIPT_FOLDER}/dev-configure.sh
${SCRIPT_FOLDER}/dev-bucket.sh ${SCRIPT_FOLDER}/dev-bucket.sh
source ${SCRIPT_FOLDER}/dev-env-aws.sh
source ${SCRIPT_FOLDER}/dev-env-s3cmd.sh
which garage
garage status garage status
garage key list garage key list
garage bucket list garage bucket list
@@ -25,58 +29,106 @@ dd if=/dev/urandom of=/tmp/garage.1.rnd bs=1k count=2 # No multipart, inline sto
dd if=/dev/urandom of=/tmp/garage.2.rnd bs=1M count=5 # No multipart but file will be chunked dd if=/dev/urandom of=/tmp/garage.2.rnd bs=1M count=5 # No multipart but file will be chunked
dd if=/dev/urandom of=/tmp/garage.3.rnd bs=1M count=10 # by default, AWS starts using multipart at 8MB dd if=/dev/urandom of=/tmp/garage.3.rnd bs=1M count=10 # by default, AWS starts using multipart at 8MB
echo "s3 api testing..." echo "🧪 S3 API testing..."
awsgrg --version
s3cmd --version
python3 --version
for idx in $(seq 1 3); do # AWS
# AWS sends if [ -z "$SKIP_AWS" ]; then
awsgrg cp "/tmp/garage.$idx.rnd" "s3://eprouvette/&+-é\"/garage.$idx.aws" echo "🛠️ Testing with awscli"
source ${SCRIPT_FOLDER}/dev-env-aws.sh
aws s3 ls
for idx in $(seq 1 3); do
aws s3 cp "/tmp/garage.$idx.rnd" "s3://eprouvette/&+-é\"/garage.$idx.aws"
aws s3 ls s3://eprouvette
aws s3 cp "s3://eprouvette/&+-é\"/garage.$idx.aws" "/tmp/garage.$idx.dl"
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
rm /tmp/garage.$idx.dl
aws s3 rm "s3://eprouvette/&+-é\"/garage.$idx.aws"
done
fi
awsgrg ls s3://eprouvette # S3CMD
if [ -z "$SKIP_S3CMD" ]; then
echo "🛠️ Testing with s3cmd"
source ${SCRIPT_FOLDER}/dev-env-s3cmd.sh
s3cmd ls
for idx in $(seq 1 3); do
s3cmd put "/tmp/garage.$idx.rnd" "s3://eprouvette/&+-é\"/garage.$idx.s3cmd"
s3cmd ls s3://eprouvette
s3cmd get "s3://eprouvette/&+-é\"/garage.$idx.s3cmd" "/tmp/garage.$idx.dl"
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
rm /tmp/garage.$idx.dl
s3cmd rm "s3://eprouvette/&+-é\"/garage.$idx.s3cmd"
done
fi
awsgrg cp "s3://eprouvette/&+-é\"/garage.$idx.aws" "/tmp/garage.$idx.dl" # Minio Client
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl if [ -z "$SKIP_MC" ]; then
rm /tmp/garage.$idx.dl echo "🛠️ Testing with mc (minio client)"
source ${SCRIPT_FOLDER}/dev-env-mc.sh
mc ls garage/
for idx in $(seq 1 3); do
mc cp "/tmp/garage.$idx.rnd" "garage/eprouvette/&+-é\"/garage.$idx.mc"
mc ls garage/eprouvette
mc cp "garage/eprouvette/&+-é\"/garage.$idx.mc" "/tmp/garage.$idx.dl"
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
rm /tmp/garage.$idx.dl
mc rm "garage/eprouvette/&+-é\"/garage.$idx.mc"
done
fi
s3grg get "s3://eprouvette/&+-é\"/garage.$idx.aws" "/tmp/garage.$idx.dl" # RClone
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl if [ -z "$SKIP_RCLONE" ]; then
rm /tmp/garage.$idx.dl echo "🛠️ Testing with rclone"
source ${SCRIPT_FOLDER}/dev-env-rclone.sh
rclone lsd garage:
for idx in $(seq 1 3); do
cp /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
rclone copy "/tmp/garage.$idx.dl" "garage:eprouvette/&+-é\"/"
rm /tmp/garage.$idx.dl
rclone ls garage:eprouvette
rclone copy "garage:eprouvette/&+-é\"/garage.$idx.dl" "/tmp/"
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
rm /tmp/garage.$idx.dl
rclone delete "garage:eprouvette/&+-é\"/garage.$idx.dl"
done
fi
awsgrg rm "s3://eprouvette/&+-é\"/garage.$idx.aws" # Duck (aka Cyberduck CLI)
if [ -z "$SKIP_DUCK" ]; then
echo "🛠️ Testing with duck (aka cyberduck cli)"
source ${SCRIPT_FOLDER}/dev-env-duck.sh
duck --list garage:/
duck --mkdir "garage:/eprouvette/duck"
for idx in $(seq 1 3); do
duck --verbose --upload "garage:/eprouvette/duck/" "/tmp/garage.$idx.rnd"
duck --list garage:/eprouvette/duck/
duck --download "garage:/eprouvette/duck/garage.$idx.rnd" "/tmp/garage.$idx.dl"
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
rm /tmp/garage.$idx.dl
duck --delete "garage:/eprouvette/duck/garage.$idx.dk"
done
fi
# S3CMD sends
s3grg put "/tmp/garage.$idx.rnd" "s3://eprouvette/&+-é\"/garage.$idx.s3cmd"
s3grg ls s3://eprouvette
s3grg get "s3://eprouvette/&+-é\"/garage.$idx.s3cmd" "/tmp/garage.$idx.dl"
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
rm /tmp/garage.$idx.dl
awsgrg cp "s3://eprouvette/&+-é\"/garage.$idx.s3cmd" "/tmp/garage.$idx.dl"
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
rm /tmp/garage.$idx.dl
s3grg rm "s3://eprouvette/&+-é\"/garage.$idx.s3cmd"
done
rm /tmp/garage.{1,2,3}.rnd rm /tmp/garage.{1,2,3}.rnd
echo "website testing" if [ -z "$SKIP_AWS" ]; then
echo "<h1>hello world</h1>" > /tmp/garage-index.html echo "🧪 Website Testing"
awsgrg cp /tmp/garage-index.html s3://eprouvette/index.html echo "<h1>hello world</h1>" > /tmp/garage-index.html
[ `curl -s -o /dev/null -w "%{http_code}" --header "Host: eprouvette.garage.tld" http://127.0.0.1:3923/ ` == 404 ] aws s3 cp /tmp/garage-index.html s3://eprouvette/index.html
garage bucket website --allow eprouvette [ `curl -s -o /dev/null -w "%{http_code}" --header "Host: eprouvette.garage.tld" http://127.0.0.1:3923/ ` == 404 ]
[ `curl -s -o /dev/null -w "%{http_code}" --header "Host: eprouvette.garage.tld" http://127.0.0.1:3923/ ` == 200 ] garage bucket website --allow eprouvette
garage bucket website --deny eprouvette [ `curl -s -o /dev/null -w "%{http_code}" --header "Host: eprouvette.garage.tld" http://127.0.0.1:3923/ ` == 200 ]
[ `curl -s -o /dev/null -w "%{http_code}" --header "Host: eprouvette.garage.tld" http://127.0.0.1:3923/ ` == 404 ] garage bucket website --deny eprouvette
awsgrg rm s3://eprouvette/index.html [ `curl -s -o /dev/null -w "%{http_code}" --header "Host: eprouvette.garage.tld" http://127.0.0.1:3923/ ` == 404 ]
rm /tmp/garage-index.html aws s3 rm s3://eprouvette/index.html
rm /tmp/garage-index.html
fi
echo "teardown" echo "🏁 Teardown"
AWS_ACCESS_KEY_ID=`cat /tmp/garage.s3 |cut -d' ' -f1`
AWS_SECRET_ACCESS_KEY=`cat /tmp/garage.s3 |cut -d' ' -f2`
garage bucket deny --read --write eprouvette --key $AWS_ACCESS_KEY_ID garage bucket deny --read --write eprouvette --key $AWS_ACCESS_KEY_ID
garage bucket delete --yes eprouvette garage bucket delete --yes eprouvette
garage key delete --yes $AWS_ACCESS_KEY_ID garage key delete --yes $AWS_ACCESS_KEY_ID
echo "success" echo "✅ Success"
+6 -4
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_api" name = "garage_api"
version = "0.2.1" version = "0.3.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
@@ -13,9 +13,9 @@ path = "lib.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
garage_model = { version = "0.2.1", path = "../model" } garage_model = { version = "0.3.0", path = "../model" }
garage_table = { version = "0.2.1", path = "../table" } garage_table = { version = "0.3.0", path = "../table" }
garage_util = { version = "0.2.1", path = "../util" } garage_util = { version = "0.3.0", path = "../util" }
base64 = "0.13" base64 = "0.13"
bytes = "1.0" bytes = "1.0"
@@ -38,4 +38,6 @@ http-range = "0.1"
hyper = "0.14" hyper = "0.14"
percent-encoding = "2.1.0" percent-encoding = "2.1.0"
roxmltree = "0.14" roxmltree = "0.14"
serde = { version = "1.0", features = ["derive"] }
quick-xml = { version = "0.21", features = [ "serialize" ] }
url = "2.1" url = "2.1"
+31 -26
View File
@@ -81,18 +81,20 @@ async fn handler(
async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Response<Body>, Error> { async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Response<Body>, Error> {
let path = req.uri().path().to_string(); let path = req.uri().path().to_string();
let path = percent_encoding::percent_decode_str(&path).decode_utf8()?; let path = percent_encoding::percent_decode_str(&path).decode_utf8()?;
let (api_key, content_sha256) = check_signature(&garage, &req).await?;
if path == "/" {
return handle_list_buckets(&api_key);
}
let (bucket, key) = parse_bucket_key(&path)?; let (bucket, key) = parse_bucket_key(&path)?;
let (api_key, content_sha256) = check_signature(&garage, &req).await?;
let allowed = match req.method() { let allowed = match req.method() {
&Method::HEAD | &Method::GET => api_key.allow_read(&bucket), &Method::HEAD | &Method::GET => api_key.allow_read(&bucket),
_ => api_key.allow_write(&bucket), _ => api_key.allow_write(&bucket),
}; };
if !allowed { if !allowed {
return Err(Error::Forbidden(format!( return Err(Error::Forbidden(
"Operation is not allowed for this key." "Operation is not allowed for this key.".to_string(),
))); ));
} }
let mut params = HashMap::new(); let mut params = HashMap::new();
@@ -104,16 +106,16 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
} }
if let Some(key) = key { if let Some(key) = key {
match req.method() { match *req.method() {
&Method::HEAD => { Method::HEAD => {
// HeadObject query // HeadObject query
Ok(handle_head(garage, &req, &bucket, &key).await?) Ok(handle_head(garage, &req, &bucket, &key).await?)
} }
&Method::GET => { Method::GET => {
// GetObject query // GetObject query
Ok(handle_get(garage, &req, &bucket, &key).await?) Ok(handle_get(garage, &req, &bucket, &key).await?)
} }
&Method::PUT => { Method::PUT => {
if params.contains_key(&"partnumber".to_string()) if params.contains_key(&"partnumber".to_string())
&& params.contains_key(&"uploadid".to_string()) && params.contains_key(&"uploadid".to_string())
{ {
@@ -152,7 +154,7 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
Ok(handle_put(garage, req, &bucket, &key, content_sha256).await?) Ok(handle_put(garage, req, &bucket, &key, content_sha256).await?)
} }
} }
&Method::DELETE => { Method::DELETE => {
if params.contains_key(&"uploadid".to_string()) { if params.contains_key(&"uploadid".to_string()) {
// AbortMultipartUpload query // AbortMultipartUpload query
let upload_id = params.get("uploadid").unwrap(); let upload_id = params.get("uploadid").unwrap();
@@ -162,7 +164,7 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
Ok(handle_delete(garage, &bucket, &key).await?) Ok(handle_delete(garage, &bucket, &key).await?)
} }
} }
&Method::POST => { Method::POST => {
if params.contains_key(&"uploads".to_string()) { if params.contains_key(&"uploads".to_string()) {
// CreateMultipartUpload call // CreateMultipartUpload call
Ok(handle_create_multipart_upload(garage, &req, &bucket, &key).await?) Ok(handle_create_multipart_upload(garage, &req, &bucket, &key).await?)
@@ -179,16 +181,16 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
) )
.await?) .await?)
} else { } else {
Err(Error::BadRequest(format!( Err(Error::BadRequest(
"Not a CreateMultipartUpload call, what is it?" "Not a CreateMultipartUpload call, what is it?".to_string(),
))) ))
} }
} }
_ => Err(Error::BadRequest(format!("Invalid method"))), _ => Err(Error::BadRequest("Invalid method".to_string())),
} }
} else { } else {
match req.method() { match *req.method() {
&Method::PUT => { Method::PUT => {
// CreateBucket // CreateBucket
// If we're here, the bucket already exists, so just answer ok // If we're here, the bucket already exists, so just answer ok
debug!( debug!(
@@ -203,29 +205,32 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
.unwrap(); .unwrap();
Ok(response) Ok(response)
} }
&Method::HEAD => { Method::HEAD => {
// HeadBucket // HeadBucket
let empty_body: Body = Body::from(vec![]); let empty_body: Body = Body::from(vec![]);
let response = Response::builder().body(empty_body).unwrap(); let response = Response::builder().body(empty_body).unwrap();
Ok(response) Ok(response)
} }
&Method::DELETE => { Method::DELETE => {
// DeleteBucket query // DeleteBucket query
Err(Error::Forbidden( Err(Error::Forbidden(
"Cannot delete buckets using S3 api, please talk to Garage directly".into(), "Cannot delete buckets using S3 api, please talk to Garage directly".into(),
)) ))
} }
&Method::GET => { Method::GET => {
if params.contains_key("location") { if params.contains_key("location") {
// GetBucketLocation call // GetBucketLocation call
Ok(handle_get_bucket_location(garage)?) Ok(handle_get_bucket_location(garage)?)
} else if params.contains_key("versioning") {
// GetBucketVersioning
Ok(handle_get_bucket_versioning()?)
} else { } else {
// ListObjects or ListObjectsV2 query // ListObjects or ListObjectsV2 query
let q = parse_list_objects_query(bucket, &params)?; let q = parse_list_objects_query(bucket, &params)?;
Ok(handle_list(garage, &q).await?) Ok(handle_list(garage, &q).await?)
} }
} }
&Method::POST => { Method::POST => {
if params.contains_key(&"delete".to_string()) { if params.contains_key(&"delete".to_string()) {
// DeleteObjects // DeleteObjects
Ok(handle_delete_objects(garage, bucket, req, content_sha256).await?) Ok(handle_delete_objects(garage, bucket, req, content_sha256).await?)
@@ -235,10 +240,10 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
std::str::from_utf8(&hyper::body::to_bytes(req.into_body()).await?) std::str::from_utf8(&hyper::body::to_bytes(req.into_body()).await?)
.unwrap_or("<invalid utf8>") .unwrap_or("<invalid utf8>")
); );
Err(Error::BadRequest(format!("Unsupported call"))) Err(Error::BadRequest("Unsupported call".to_string()))
} }
} }
_ => Err(Error::BadRequest(format!("Invalid method"))), _ => Err(Error::BadRequest("Invalid method".to_string())),
} }
} }
} }
@@ -253,7 +258,7 @@ fn parse_bucket_key(path: &str) -> Result<(&str, Option<&str>), Error> {
let (bucket, key) = match path.find('/') { let (bucket, key) = match path.find('/') {
Some(i) => { Some(i) => {
let key = &path[i + 1..]; let key = &path[i + 1..];
if key.len() > 0 { if !key.is_empty() {
(&path[..i], Some(key)) (&path[..i], Some(key))
} else { } else {
(&path[..i], None) (&path[..i], None)
@@ -261,8 +266,8 @@ fn parse_bucket_key(path: &str) -> Result<(&str, Option<&str>), Error> {
} }
None => (path, None), None => (path, None),
}; };
if bucket.len() == 0 { if bucket.is_empty() {
return Err(Error::BadRequest(format!("No bucket specified"))); return Err(Error::BadRequest("No bucket specified".to_string()));
} }
Ok((bucket, key)) Ok((bucket, key))
} }
-17
View File
@@ -1,13 +1,5 @@
//! Module containing various helpers for encoding //! Module containing various helpers for encoding
/// Escape &str for xml inclusion
pub fn xml_escape(s: &str) -> String {
s.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
}
/// Encode &str for use in a URI /// Encode &str for use in a URI
pub fn uri_encode(string: &str, encode_slash: bool) -> String { pub fn uri_encode(string: &str, encode_slash: bool) -> String {
let mut result = String::with_capacity(string.len() * 2); let mut result = String::with_capacity(string.len() * 2);
@@ -28,12 +20,3 @@ pub fn uri_encode(string: &str, encode_slash: bool) -> String {
} }
result result
} }
/// Encode &str either as an uri, or a valid string for xml inclusion
pub fn xml_encode_key(k: &str, urlencode: bool) -> String {
if urlencode {
uri_encode(k, true)
} else {
xml_escape(k)
}
}
+36 -26
View File
@@ -1,11 +1,9 @@
use std::fmt::Write;
use err_derive::Error; use err_derive::Error;
use hyper::StatusCode; use hyper::StatusCode;
use garage_util::error::Error as GarageError; use garage_util::error::Error as GarageError;
use crate::encoding::*; use crate::s3_xml;
/// Errors of this crate /// Errors of this crate
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -21,7 +19,7 @@ pub enum Error {
/// Error related to HTTP /// Error related to HTTP
#[error(display = "Internal error (HTTP error): {}", _0)] #[error(display = "Internal error (HTTP error): {}", _0)]
HTTP(#[error(source)] http::Error), Http(#[error(source)] http::Error),
// Category: cannot process // Category: cannot process
/// No proper api key was used, or the signature was invalid /// No proper api key was used, or the signature was invalid
@@ -39,11 +37,11 @@ pub enum Error {
// Category: bad request // Category: bad request
/// The request contained an invalid UTF-8 sequence in its path or in other parameters /// The request contained an invalid UTF-8 sequence in its path or in other parameters
#[error(display = "Invalid UTF-8: {}", _0)] #[error(display = "Invalid UTF-8: {}", _0)]
InvalidUTF8Str(#[error(source)] std::str::Utf8Error), InvalidUtf8Str(#[error(source)] std::str::Utf8Error),
/// The request used an invalid path /// The request used an invalid path
#[error(display = "Invalid UTF-8: {}", _0)] #[error(display = "Invalid UTF-8: {}", _0)]
InvalidUTF8String(#[error(source)] std::string::FromUtf8Error), InvalidUtf8String(#[error(source)] std::string::FromUtf8Error),
/// Some base64 encoded data was badly encoded /// Some base64 encoded data was badly encoded
#[error(display = "Invalid base64: {}", _0)] #[error(display = "Invalid base64: {}", _0)]
@@ -51,7 +49,7 @@ pub enum Error {
/// The client sent invalid XML data /// The client sent invalid XML data
#[error(display = "Invalid XML: {}", _0)] #[error(display = "Invalid XML: {}", _0)]
InvalidXML(String), InvalidXml(String),
/// The client sent a header with invalid value /// The client sent a header with invalid value
#[error(display = "Invalid header value: {}", _0)] #[error(display = "Invalid header value: {}", _0)]
@@ -68,7 +66,13 @@ pub enum Error {
impl From<roxmltree::Error> for Error { impl From<roxmltree::Error> for Error {
fn from(err: roxmltree::Error) -> Self { fn from(err: roxmltree::Error) -> Self {
Self::InvalidXML(format!("{}", err)) Self::InvalidXml(format!("{}", err))
}
}
impl From<quick_xml::de::DeError> for Error {
fn from(err: quick_xml::de::DeError) -> Self {
Self::InvalidXml(format!("{}", err))
} }
} }
@@ -78,8 +82,8 @@ impl Error {
match self { match self {
Error::NotFound => StatusCode::NOT_FOUND, Error::NotFound => StatusCode::NOT_FOUND,
Error::Forbidden(_) => StatusCode::FORBIDDEN, Error::Forbidden(_) => StatusCode::FORBIDDEN,
Error::InternalError(GarageError::RPC(_)) => StatusCode::SERVICE_UNAVAILABLE, Error::InternalError(GarageError::Rpc(_)) => StatusCode::SERVICE_UNAVAILABLE,
Error::InternalError(_) | Error::Hyper(_) | Error::HTTP(_) => { Error::InternalError(_) | Error::Hyper(_) | Error::Http(_) => {
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
} }
_ => StatusCode::BAD_REQUEST, _ => StatusCode::BAD_REQUEST,
@@ -91,22 +95,29 @@ impl Error {
Error::NotFound => "NoSuchKey", Error::NotFound => "NoSuchKey",
Error::Forbidden(_) => "AccessDenied", Error::Forbidden(_) => "AccessDenied",
Error::AuthorizationHeaderMalformed(_) => "AuthorizationHeaderMalformed", Error::AuthorizationHeaderMalformed(_) => "AuthorizationHeaderMalformed",
Error::InternalError(GarageError::RPC(_)) => "ServiceUnavailable", Error::InternalError(GarageError::Rpc(_)) => "ServiceUnavailable",
Error::InternalError(_) | Error::Hyper(_) | Error::HTTP(_) => "InternalError", Error::InternalError(_) | Error::Hyper(_) | Error::Http(_) => "InternalError",
_ => "InvalidRequest", _ => "InvalidRequest",
} }
} }
pub fn aws_xml(&self, garage_region: &str, path: &str) -> String { pub fn aws_xml(&self, garage_region: &str, path: &str) -> String {
let mut xml = String::new(); let error = s3_xml::Error {
writeln!(&mut xml, r#"<?xml version="1.0" encoding="UTF-8"?>"#).unwrap(); code: s3_xml::Value(self.aws_code().to_string()),
writeln!(&mut xml, "<Error>").unwrap(); message: s3_xml::Value(format!("{}", self)),
writeln!(&mut xml, "\t<Code>{}</Code>", self.aws_code()).unwrap(); resource: Some(s3_xml::Value(path.to_string())),
writeln!(&mut xml, "\t<Message>{}</Message>", self).unwrap(); region: Some(s3_xml::Value(garage_region.to_string())),
writeln!(&mut xml, "\t<Resource>{}</Resource>", xml_escape(path)).unwrap(); };
writeln!(&mut xml, "\t<Region>{}</Region>", garage_region).unwrap(); s3_xml::to_xml_with_header(&error).unwrap_or_else(|_| {
writeln!(&mut xml, "</Error>").unwrap(); r#"
xml <?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>InternalError</Code>
<Message>XML encoding of error failed</Message>
</Error>
"#
.into()
})
} }
} }
@@ -134,7 +145,7 @@ impl<T> OkOrBadRequest for Option<T> {
fn ok_or_bad_request(self, reason: &'static str) -> Result<T, Error> { fn ok_or_bad_request(self, reason: &'static str) -> Result<T, Error> {
match self { match self {
Some(x) => Ok(x), Some(x) => Ok(x),
None => Err(Error::BadRequest(format!("{}", reason))), None => Err(Error::BadRequest(reason.to_string())),
} }
} }
} }
@@ -166,10 +177,9 @@ impl<T> OkOrInternalError for Option<T> {
fn ok_or_internal_error(self, reason: &'static str) -> Result<T, Error> { fn ok_or_internal_error(self, reason: &'static str) -> Result<T, Error> {
match self { match self {
Some(x) => Ok(x), Some(x) => Ok(x),
None => Err(Error::InternalError(GarageError::Message(format!( None => Err(Error::InternalError(GarageError::Message(
"{}", reason.to_string(),
reason ))),
)))),
} }
} }
} }
+1
View File
@@ -18,3 +18,4 @@ mod s3_delete;
pub mod s3_get; pub mod s3_get;
mod s3_list; mod s3_list;
mod s3_put; mod s3_put;
mod s3_xml;
+48 -10
View File
@@ -1,24 +1,62 @@
use std::fmt::Write;
use std::sync::Arc; use std::sync::Arc;
use hyper::{Body, Response}; use hyper::{Body, Response};
use garage_model::garage::Garage; use garage_model::garage::Garage;
use garage_model::key_table::Key;
use garage_util::time::*;
use crate::error::*; use crate::error::*;
use crate::s3_xml;
pub fn handle_get_bucket_location(garage: Arc<Garage>) -> Result<Response<Body>, Error> { pub fn handle_get_bucket_location(garage: Arc<Garage>) -> Result<Response<Body>, Error> {
let mut xml = String::new(); let loc = s3_xml::LocationConstraint {
xmlns: (),
writeln!(&mut xml, r#"<?xml version="1.0" encoding="UTF-8"?>"#).unwrap(); region: garage.config.s3_api.s3_region.to_string(),
writeln!( };
&mut xml, let xml = s3_xml::to_xml_with_header(&loc)?;
r#"<LocationConstraint xmlns="http://s3.amazonaws.com/doc/2006-03-01/">{}</LocationConstraint>"#,
garage.config.s3_api.s3_region
)
.unwrap();
Ok(Response::builder() Ok(Response::builder()
.header("Content-Type", "application/xml") .header("Content-Type", "application/xml")
.body(Body::from(xml.into_bytes()))?) .body(Body::from(xml.into_bytes()))?)
} }
pub fn handle_get_bucket_versioning() -> Result<Response<Body>, Error> {
let versioning = s3_xml::VersioningConfiguration {
xmlns: (),
status: None,
};
let xml = s3_xml::to_xml_with_header(&versioning)?;
Ok(Response::builder()
.header("Content-Type", "application/xml")
.body(Body::from(xml.into_bytes()))?)
}
pub fn handle_list_buckets(api_key: &Key) -> Result<Response<Body>, Error> {
let list_buckets = s3_xml::ListAllMyBucketsResult {
owner: s3_xml::Owner {
display_name: s3_xml::Value(api_key.name.get().to_string()),
id: s3_xml::Value(api_key.key_id.to_string()),
},
buckets: s3_xml::BucketList {
entries: api_key
.authorized_buckets
.items()
.iter()
.map(|(name, ts, _)| s3_xml::Bucket {
creation_date: s3_xml::Value(msec_to_rfc3339(*ts)),
name: s3_xml::Value(name.to_string()),
})
.collect(),
},
};
let xml = s3_xml::to_xml_with_header(&list_buckets)?;
trace!("xml: {}", xml);
Ok(Response::builder()
.header("Content-Type", "application/xml")
.body(Body::from(xml))?)
}
+10 -9
View File
@@ -1,4 +1,3 @@
use std::fmt::Write;
use std::sync::Arc; use std::sync::Arc;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
@@ -14,6 +13,7 @@ use garage_model::version_table::*;
use crate::error::*; use crate::error::*;
use crate::s3_put::get_headers; use crate::s3_put::get_headers;
use crate::s3_xml;
pub async fn handle_copy( pub async fn handle_copy(
garage: Arc<Garage>, garage: Arc<Garage>,
@@ -33,8 +33,7 @@ pub async fn handle_copy(
.versions() .versions()
.iter() .iter()
.rev() .rev()
.filter(|v| v.is_complete()) .find(|v| v.is_complete())
.next()
.ok_or(Error::NotFound)?; .ok_or(Error::NotFound)?;
let source_last_state = match &source_last_v.state { let source_last_state = match &source_last_v.state {
@@ -62,6 +61,8 @@ pub async fn handle_copy(
_ => old_meta.clone(), _ => old_meta.clone(),
}; };
let etag = new_meta.etag.to_string();
// Save object copy // Save object copy
match source_last_state { match source_last_state {
ObjectVersionData::DeleteMarker => unreachable!(), ObjectVersionData::DeleteMarker => unreachable!(),
@@ -159,13 +160,13 @@ pub async fn handle_copy(
} }
let last_modified = msec_to_rfc3339(new_timestamp); let last_modified = msec_to_rfc3339(new_timestamp);
let mut xml = String::new(); let result = s3_xml::CopyObjectResult {
writeln!(&mut xml, r#"<?xml version="1.0" encoding="UTF-8"?>"#).unwrap(); last_modified: s3_xml::Value(last_modified),
writeln!(&mut xml, r#"<CopyObjectResult>"#).unwrap(); etag: s3_xml::Value(etag),
writeln!(&mut xml, "\t<LastModified>{}</LastModified>", last_modified).unwrap(); };
writeln!(&mut xml, "</CopyObjectResult>").unwrap(); let xml = s3_xml::to_xml_with_header(&result)?;
Ok(Response::builder() Ok(Response::builder()
.header("Content-Type", "application/xml") .header("Content-Type", "application/xml")
.body(Body::from(xml.into_bytes()))?) .body(Body::from(xml))?)
} }
+27 -41
View File
@@ -1,4 +1,3 @@
use std::fmt::Write;
use std::sync::Arc; use std::sync::Arc;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
@@ -9,25 +8,27 @@ use garage_util::time::*;
use garage_model::garage::Garage; use garage_model::garage::Garage;
use garage_model::object_table::*; use garage_model::object_table::*;
use crate::encoding::*;
use crate::error::*; use crate::error::*;
use crate::s3_xml;
use crate::signature::verify_signed_content; use crate::signature::verify_signed_content;
async fn handle_delete_internal( async fn handle_delete_internal(
garage: &Garage, garage: &Garage,
bucket: &str, bucket: &str,
key: &str, key: &str,
) -> Result<(UUID, UUID), Error> { ) -> Result<(Uuid, Uuid), Error> {
let object = garage let object = garage
.object_table .object_table
.get(&bucket.to_string(), &key.to_string()) .get(&bucket.to_string(), &key.to_string())
.await? .await?
.ok_or(Error::NotFound)?; // No need to delete .ok_or(Error::NotFound)?; // No need to delete
let interesting_versions = object.versions().iter().filter(|v| match v.state { let interesting_versions = object.versions().iter().filter(|v| {
ObjectVersionState::Aborted => false, !matches!(
ObjectVersionState::Complete(ObjectVersionData::DeleteMarker) => false, v.state,
_ => true, ObjectVersionState::Aborted
| ObjectVersionState::Complete(ObjectVersionData::DeleteMarker)
)
}); });
let mut version_to_delete = None; let mut version_to_delete = None;
@@ -83,13 +84,8 @@ pub async fn handle_delete_objects(
let cmd_xml = roxmltree::Document::parse(&std::str::from_utf8(&body)?)?; let cmd_xml = roxmltree::Document::parse(&std::str::from_utf8(&body)?)?;
let cmd = parse_delete_objects_xml(&cmd_xml).ok_or_bad_request("Invalid delete XML query")?; let cmd = parse_delete_objects_xml(&cmd_xml).ok_or_bad_request("Invalid delete XML query")?;
let mut retxml = String::new(); let mut ret_deleted = Vec::new();
writeln!(&mut retxml, r#"<?xml version="1.0" encoding="UTF-8"?>"#).unwrap(); let mut ret_errors = Vec::new();
writeln!(
&mut retxml,
r#"<DeleteResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">"#
)
.unwrap();
for obj in cmd.objects.iter() { for obj in cmd.objects.iter() {
match handle_delete_internal(&garage, bucket, &obj.key).await { match handle_delete_internal(&garage, bucket, &obj.key).await {
@@ -97,42 +93,32 @@ pub async fn handle_delete_objects(
if cmd.quiet { if cmd.quiet {
continue; continue;
} }
writeln!(&mut retxml, "\t<Deleted>").unwrap(); ret_deleted.push(s3_xml::Deleted {
writeln!(&mut retxml, "\t\t<Key>{}</Key>", xml_escape(&obj.key)).unwrap(); key: s3_xml::Value(obj.key.clone()),
writeln!( version_id: s3_xml::Value(hex::encode(deleted_version)),
&mut retxml, delete_marker_version_id: s3_xml::Value(hex::encode(delete_marker_version)),
"\t\t<VersionId>{}</VersionId>", });
hex::encode(deleted_version)
)
.unwrap();
writeln!(
&mut retxml,
"\t\t<DeleteMarkerVersionId>{}</DeleteMarkerVersionId>",
hex::encode(delete_marker_version)
)
.unwrap();
writeln!(&mut retxml, "\t</Deleted>").unwrap();
} }
Err(e) => { Err(e) => {
writeln!(&mut retxml, "\t<Error>").unwrap(); ret_errors.push(s3_xml::DeleteError {
writeln!(&mut retxml, "\t\t<Code>{}</Code>", e.http_status_code()).unwrap(); code: s3_xml::Value(e.aws_code().to_string()),
writeln!(&mut retxml, "\t\t<Key>{}</Key>", xml_escape(&obj.key)).unwrap(); key: Some(s3_xml::Value(obj.key.clone())),
writeln!( message: s3_xml::Value(format!("{}", e)),
&mut retxml, version_id: None,
"\t\t<Message>{}</Message>", });
xml_escape(&format!("{}", e))
)
.unwrap();
writeln!(&mut retxml, "\t</Error>").unwrap();
} }
} }
} }
writeln!(&mut retxml, "</DeleteResult>").unwrap(); let xml = s3_xml::to_xml_with_header(&s3_xml::DeleteResult {
xmlns: (),
deleted: ret_deleted,
errors: ret_errors,
})?;
Ok(Response::builder() Ok(Response::builder()
.header("Content-Type", "application/xml") .header("Content-Type", "application/xml")
.body(Body::from(retxml.into_bytes()))?) .body(Body::from(xml))?)
} }
struct DeleteRequest { struct DeleteRequest {
+10 -10
View File
@@ -28,7 +28,7 @@ fn object_headers(
version_meta.headers.content_type.to_string(), version_meta.headers.content_type.to_string(),
) )
.header("Last-Modified", date_str) .header("Last-Modified", date_str)
.header("Accept-Ranges", format!("bytes")); .header("Accept-Ranges", "bytes".to_string());
if !version_meta.etag.is_empty() { if !version_meta.etag.is_empty() {
resp = resp.header("ETag", format!("\"{}\"", version_meta.etag)); resp = resp.header("ETag", format!("\"{}\"", version_meta.etag));
@@ -97,8 +97,7 @@ pub async fn handle_head(
.versions() .versions()
.iter() .iter()
.rev() .rev()
.filter(|v| v.is_data()) .find(|v| v.is_data())
.next()
.ok_or(Error::NotFound)?; .ok_or(Error::NotFound)?;
let version_meta = match &version.state { let version_meta = match &version.state {
@@ -137,8 +136,7 @@ pub async fn handle_get(
.versions() .versions()
.iter() .iter()
.rev() .rev()
.filter(|v| v.is_complete()) .find(|v| v.is_complete())
.next()
.ok_or(Error::NotFound)?; .ok_or(Error::NotFound)?;
let last_v_data = match &last_v.state { let last_v_data = match &last_v.state {
@@ -160,7 +158,9 @@ pub async fn handle_get(
let range_str = range.to_str()?; let range_str = range.to_str()?;
let mut ranges = http_range::HttpRange::parse(range_str, last_v_meta.size)?; let mut ranges = http_range::HttpRange::parse(range_str, last_v_meta.size)?;
if ranges.len() > 1 { if ranges.len() > 1 {
return Err(Error::BadRequest(format!("Multiple ranges not supported"))); return Err(Error::BadRequest(
"Multiple ranges not supported".to_string(),
));
} else { } else {
ranges.pop() ranges.pop()
} }
@@ -236,7 +236,7 @@ async fn handle_get_range(
end: u64, end: u64,
) -> Result<Response<Body>, Error> { ) -> Result<Response<Body>, Error> {
if end > version_meta.size { if end > version_meta.size {
return Err(Error::BadRequest(format!("Range not included in file"))); return Err(Error::BadRequest("Range not included in file".to_string()));
} }
let resp_builder = object_headers(version, version_meta) let resp_builder = object_headers(version, version_meta)
@@ -282,7 +282,7 @@ async fn handle_get_range(
} }
// Keep only blocks that have an intersection with the requested range // Keep only blocks that have an intersection with the requested range
if true_offset < end && true_offset + b.size > begin { if true_offset < end && true_offset + b.size > begin {
blocks.push((b.clone(), true_offset)); blocks.push((*b, true_offset));
} }
true_offset += b.size; true_offset += b.size;
} }
@@ -303,9 +303,9 @@ async fn handle_get_range(
} else { } else {
end - true_offset end - true_offset
}; };
Result::<Bytes, Error>::Ok(Bytes::from( Result::<Bytes, Error>::Ok(
data.slice(start_in_block as usize..end_in_block as usize), data.slice(start_in_block as usize..end_in_block as usize),
)) )
} }
}) })
.buffered(2); .buffered(2);
+57 -99
View File
@@ -1,5 +1,4 @@
use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::fmt::Write;
use std::sync::Arc; use std::sync::Arc;
use hyper::{Body, Response}; use hyper::{Body, Response};
@@ -14,6 +13,7 @@ use garage_table::DeletedFilter;
use crate::encoding::*; use crate::encoding::*;
use crate::error::*; use crate::error::*;
use crate::s3_xml;
#[derive(Debug)] #[derive(Debug)]
pub struct ListObjectsQuery { pub struct ListObjectsQuery {
@@ -50,7 +50,7 @@ pub fn parse_list_objects_query(
.ok_or_bad_request("Invalid value for max-keys") .ok_or_bad_request("Invalid value for max-keys")
}) })
.unwrap_or(Ok(1000))?, .unwrap_or(Ok(1000))?,
prefix: params.get("prefix").cloned().unwrap_or(String::new()), prefix: params.get("prefix").cloned().unwrap_or_default(),
marker: params.get("marker").cloned(), marker: params.get("marker").cloned(),
continuation_token: params.get("continuation-token").cloned(), continuation_token: params.get("continuation-token").cloned(),
start_after: params.get("start-after").cloned(), start_after: params.get("start-after").cloned(),
@@ -72,10 +72,13 @@ pub async fn handle_list(
if let Some(ct) = &query.continuation_token { if let Some(ct) = &query.continuation_token {
String::from_utf8(base64::decode(ct.as_bytes())?)? String::from_utf8(base64::decode(ct.as_bytes())?)?
} else { } else {
query.start_after.clone().unwrap_or(query.prefix.clone()) query
.start_after
.clone()
.unwrap_or_else(|| query.prefix.clone())
} }
} else { } else {
query.marker.clone().unwrap_or(query.prefix.clone()) query.marker.clone().unwrap_or_else(|| query.prefix.clone())
}; };
debug!( debug!(
@@ -155,131 +158,86 @@ pub async fn handle_list(
truncated = None; truncated = None;
break 'query_loop; break 'query_loop;
} }
if objects.len() > 0 { if !objects.is_empty() {
next_chunk_start = objects[objects.len() - 1].key.clone(); next_chunk_start = objects[objects.len() - 1].key.clone();
} }
} }
let mut xml = String::new(); let mut result = s3_xml::ListBucketResult {
writeln!(&mut xml, r#"<?xml version="1.0" encoding="UTF-8"?>"#).unwrap(); xmlns: (),
writeln!( name: s3_xml::Value(query.bucket.to_string()),
&mut xml, prefix: uriencode_maybe(&query.prefix, query.urlencode_resp),
r#"<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">"# marker: None,
) next_marker: None,
.unwrap(); start_after: None,
continuation_token: None,
next_continuation_token: None,
max_keys: s3_xml::IntValue(query.max_keys as i64),
delimiter: query
.delimiter
.as_ref()
.map(|x| uriencode_maybe(x, query.urlencode_resp)),
encoding_type: match query.urlencode_resp {
true => Some(s3_xml::Value("url".to_string())),
false => None,
},
writeln!(&mut xml, "\t<Name>{}</Name>", query.bucket).unwrap(); key_count: Some(s3_xml::IntValue(
result_keys.len() as i64 + result_common_prefixes.len() as i64,
// TODO: in V1, is this supposed to be urlencoded when encoding-type is URL?? )),
writeln!( is_truncated: s3_xml::Value(format!("{}", truncated.is_some())),
&mut xml, contents: vec![],
"\t<Prefix>{}</Prefix>", common_prefixes: vec![],
xml_encode_key(&query.prefix, query.urlencode_resp), };
)
.unwrap();
if let Some(delim) = &query.delimiter {
// TODO: in V1, is this supposed to be urlencoded when encoding-type is URL??
writeln!(
&mut xml,
"\t<Delimiter>{}</Delimiter>",
xml_encode_key(delim, query.urlencode_resp),
)
.unwrap();
}
writeln!(&mut xml, "\t<MaxKeys>{}</MaxKeys>", query.max_keys).unwrap();
if query.urlencode_resp {
writeln!(&mut xml, "\t<EncodingType>url</EncodingType>").unwrap();
}
writeln!(
&mut xml,
"\t<KeyCount>{}</KeyCount>",
result_keys.len() + result_common_prefixes.len()
)
.unwrap();
writeln!(
&mut xml,
"\t<IsTruncated>{}</IsTruncated>",
truncated.is_some()
)
.unwrap();
if query.is_v2 { if query.is_v2 {
if let Some(ct) = &query.continuation_token { if let Some(ct) = &query.continuation_token {
writeln!(&mut xml, "\t<ContinuationToken>{}</ContinuationToken>", ct).unwrap(); result.continuation_token = Some(s3_xml::Value(ct.to_string()));
} }
if let Some(sa) = &query.start_after { if let Some(sa) = &query.start_after {
writeln!( result.start_after = Some(uriencode_maybe(sa, query.urlencode_resp));
&mut xml,
"\t<StartAfter>{}</StartAfter>",
xml_encode_key(sa, query.urlencode_resp)
)
.unwrap();
} }
if let Some(nct) = truncated { if let Some(nct) = truncated {
writeln!( result.next_continuation_token = Some(s3_xml::Value(base64::encode(nct.as_bytes())));
&mut xml,
"\t<NextContinuationToken>{}</NextContinuationToken>",
base64::encode(nct.as_bytes())
)
.unwrap();
} }
} else { } else {
// TODO: are these supposed to be urlencoded when encoding-type is URL?? // TODO: are these supposed to be urlencoded when encoding-type is URL??
if let Some(mkr) = &query.marker { if let Some(mkr) = &query.marker {
writeln!( result.marker = Some(uriencode_maybe(mkr, query.urlencode_resp));
&mut xml,
"\t<Marker>{}</Marker>",
xml_encode_key(mkr, query.urlencode_resp)
)
.unwrap();
} }
if let Some(next_marker) = truncated { if let Some(next_marker) = truncated {
writeln!( result.next_marker = Some(uriencode_maybe(&next_marker, query.urlencode_resp));
&mut xml,
"\t<NextMarker>{}</NextMarker>",
xml_encode_key(&next_marker, query.urlencode_resp)
)
.unwrap();
} }
} }
for (key, info) in result_keys.iter() { for (key, info) in result_keys.iter() {
let last_modif = msec_to_rfc3339(info.last_modified); result.contents.push(s3_xml::ListBucketItem {
writeln!(&mut xml, "\t<Contents>").unwrap(); key: uriencode_maybe(key, query.urlencode_resp),
writeln!( last_modified: s3_xml::Value(msec_to_rfc3339(info.last_modified)),
&mut xml, size: s3_xml::IntValue(info.size as i64),
"\t\t<Key>{}</Key>", etag: s3_xml::Value(info.etag.to_string()),
xml_encode_key(key, query.urlencode_resp), storage_class: s3_xml::Value("STANDARD".to_string()),
) });
.unwrap();
writeln!(&mut xml, "\t\t<LastModified>{}</LastModified>", last_modif).unwrap();
writeln!(&mut xml, "\t\t<Size>{}</Size>", info.size).unwrap();
if !info.etag.is_empty() {
writeln!(&mut xml, "\t\t<ETag>\"{}\"</ETag>", info.etag).unwrap();
}
writeln!(&mut xml, "\t\t<StorageClass>STANDARD</StorageClass>").unwrap();
writeln!(&mut xml, "\t</Contents>").unwrap();
} }
for pfx in result_common_prefixes.iter() { for pfx in result_common_prefixes.iter() {
writeln!(&mut xml, "\t<CommonPrefixes>").unwrap();
//TODO: in V1, are these urlencoded when urlencode_resp is true ?? (proably) //TODO: in V1, are these urlencoded when urlencode_resp is true ?? (proably)
writeln!( result.common_prefixes.push(s3_xml::CommonPrefix {
&mut xml, prefix: uriencode_maybe(pfx, query.urlencode_resp),
"\t\t<Prefix>{}</Prefix>", });
xml_encode_key(pfx, query.urlencode_resp),
)
.unwrap();
writeln!(&mut xml, "\t</CommonPrefixes>").unwrap();
} }
writeln!(&mut xml, "</ListBucketResult>").unwrap(); let xml = s3_xml::to_xml_with_header(&result)?;
debug!("{}", xml);
Ok(Response::builder() Ok(Response::builder()
.header("Content-Type", "application/xml") .header("Content-Type", "application/xml")
.body(Body::from(xml.into_bytes()))?) .body(Body::from(xml.into_bytes()))?)
} }
fn uriencode_maybe(s: &str, yes: bool) -> s3_xml::Value {
if yes {
s3_xml::Value(uri_encode(s, true))
} else {
s3_xml::Value(s.to_string())
}
}
+40 -54
View File
@@ -1,5 +1,4 @@
use std::collections::{BTreeMap, VecDeque}; use std::collections::{BTreeMap, VecDeque};
use std::fmt::Write;
use std::sync::Arc; use std::sync::Arc;
use futures::stream::*; use futures::stream::*;
@@ -18,8 +17,8 @@ use garage_model::garage::Garage;
use garage_model::object_table::*; use garage_model::object_table::*;
use garage_model::version_table::*; use garage_model::version_table::*;
use crate::encoding::*;
use crate::error::*; use crate::error::*;
use crate::s3_xml;
use crate::signature::verify_signed_content; use crate::signature::verify_signed_content;
pub async fn handle_put( pub async fn handle_put(
@@ -46,7 +45,7 @@ pub async fn handle_put(
let body = req.into_body(); let body = req.into_body();
let mut chunker = BodyChunker::new(body, garage.config.block_size); let mut chunker = BodyChunker::new(body, garage.config.block_size);
let first_block = chunker.next().await?.unwrap_or(vec![]); let first_block = chunker.next().await?.unwrap_or_default();
// If body is small enough, store it directly in the object table // If body is small enough, store it directly in the object table
// as "inline data". We can then return immediately. // as "inline data". We can then return immediately.
@@ -160,16 +159,18 @@ fn ensure_checksum_matches(
) -> Result<(), Error> { ) -> Result<(), Error> {
if let Some(expected_sha256) = content_sha256 { if let Some(expected_sha256) = content_sha256 {
if expected_sha256 != data_sha256sum { if expected_sha256 != data_sha256sum {
return Err(Error::BadRequest(format!( return Err(Error::BadRequest(
"Unable to validate x-amz-content-sha256" "Unable to validate x-amz-content-sha256".to_string(),
))); ));
} else { } else {
trace!("Successfully validated x-amz-content-sha256"); trace!("Successfully validated x-amz-content-sha256");
} }
} }
if let Some(expected_md5) = content_md5 { if let Some(expected_md5) = content_md5 {
if expected_md5.trim_matches('"') != base64::encode(data_md5sum) { if expected_md5.trim_matches('"') != base64::encode(data_md5sum) {
return Err(Error::BadRequest(format!("Unable to validate content-md5"))); return Err(Error::BadRequest(
"Unable to validate content-md5".to_string(),
));
} else { } else {
trace!("Successfully validated content-md5"); trace!("Successfully validated content-md5");
} }
@@ -291,7 +292,7 @@ impl BodyChunker {
self.read_all = true; self.read_all = true;
} }
} }
if self.buf.len() == 0 { if self.buf.is_empty() {
Ok(None) Ok(None)
} else if self.buf.len() <= self.block_size { } else if self.buf.len() <= self.block_size {
let block = self.buf.drain(..).collect::<Vec<u8>>(); let block = self.buf.drain(..).collect::<Vec<u8>>();
@@ -303,7 +304,7 @@ impl BodyChunker {
} }
} }
pub fn put_response(version_uuid: UUID, md5sum_hex: String) -> Response<Body> { pub fn put_response(version_uuid: Uuid, md5sum_hex: String) -> Response<Body> {
Response::builder() Response::builder()
.header("x-amz-version-id", hex::encode(version_uuid)) .header("x-amz-version-id", hex::encode(version_uuid))
.header("ETag", format!("\"{}\"", md5sum_hex)) .header("ETag", format!("\"{}\"", md5sum_hex))
@@ -337,22 +338,13 @@ pub async fn handle_create_multipart_upload(
garage.version_table.insert(&version).await?; garage.version_table.insert(&version).await?;
// Send success response // Send success response
let mut xml = String::new(); let result = s3_xml::InitiateMultipartUploadResult {
writeln!(&mut xml, r#"<?xml version="1.0" encoding="UTF-8"?>"#).unwrap(); xmlns: (),
writeln!( bucket: s3_xml::Value(bucket.to_string()),
&mut xml, key: s3_xml::Value(key.to_string()),
r#"<InitiateMultipartUploadResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">"# upload_id: s3_xml::Value(hex::encode(version_uuid)),
) };
.unwrap(); let xml = s3_xml::to_xml_with_header(&result)?;
writeln!(&mut xml, "\t<Bucket>{}</Bucket>", bucket).unwrap();
writeln!(&mut xml, "\t<Key>{}</Key>", xml_escape(key)).unwrap();
writeln!(
&mut xml,
"\t<UploadId>{}</UploadId>",
hex::encode(version_uuid)
)
.unwrap();
writeln!(&mut xml, "</InitiateMultipartUploadResult>").unwrap();
Ok(Response::new(Body::from(xml.into_bytes()))) Ok(Response::new(Body::from(xml.into_bytes())))
} }
@@ -387,8 +379,8 @@ pub async fn handle_put_part(
futures::try_join!(garage.object_table.get(&bucket, &key), chunker.next(),)?; futures::try_join!(garage.object_table.get(&bucket, &key), chunker.next(),)?;
// Check object is valid and multipart block can be accepted // Check object is valid and multipart block can be accepted
let first_block = first_block.ok_or(Error::BadRequest(format!("Empty body")))?; let first_block = first_block.ok_or_else(|| Error::BadRequest("Empty body".to_string()))?;
let object = object.ok_or(Error::BadRequest(format!("Object not found")))?; let object = object.ok_or_else(|| Error::BadRequest("Object not found".to_string()))?;
if !object if !object
.versions() .versions()
@@ -462,21 +454,21 @@ pub async fn handle_complete_multipart_upload(
garage.version_table.get(&version_uuid, &EmptyKey), garage.version_table.get(&version_uuid, &EmptyKey),
)?; )?;
let object = object.ok_or(Error::BadRequest(format!("Object not found")))?; let object = object.ok_or_else(|| Error::BadRequest("Object not found".to_string()))?;
let mut object_version = object let mut object_version = object
.versions() .versions()
.iter() .iter()
.find(|v| v.uuid == version_uuid && v.is_uploading()) .find(|v| v.uuid == version_uuid && v.is_uploading())
.cloned() .cloned()
.ok_or(Error::BadRequest(format!("Version not found")))?; .ok_or_else(|| Error::BadRequest("Version not found".to_string()))?;
let version = version.ok_or(Error::BadRequest(format!("Version not found")))?; let version = version.ok_or_else(|| Error::BadRequest("Version not found".to_string()))?;
if version.blocks.len() == 0 { if version.blocks.is_empty() {
return Err(Error::BadRequest(format!("No data was uploaded"))); return Err(Error::BadRequest("No data was uploaded".to_string()));
} }
let headers = match object_version.state { let headers = match object_version.state {
ObjectVersionState::Uploading(headers) => headers.clone(), ObjectVersionState::Uploading(headers) => headers,
_ => unreachable!(), _ => unreachable!(),
}; };
@@ -493,7 +485,9 @@ pub async fn handle_complete_multipart_upload(
.map(|x| (&x.part_number, &x.etag)) .map(|x| (&x.part_number, &x.etag))
.eq(parts); .eq(parts);
if !same_parts { if !same_parts {
return Err(Error::BadRequest(format!("We don't have the same parts"))); return Err(Error::BadRequest(
"We don't have the same parts".to_string(),
));
} }
// Calculate etag of final object // Calculate etag of final object
@@ -516,7 +510,7 @@ pub async fn handle_complete_multipart_upload(
ObjectVersionMeta { ObjectVersionMeta {
headers, headers,
size: total_size, size: total_size,
etag, etag: etag.clone(),
}, },
version.blocks.items()[0].1.hash, version.blocks.items()[0].1.hash,
)); ));
@@ -525,22 +519,14 @@ pub async fn handle_complete_multipart_upload(
garage.object_table.insert(&final_object).await?; garage.object_table.insert(&final_object).await?;
// Send response saying ok we're done // Send response saying ok we're done
let mut xml = String::new(); let result = s3_xml::CompleteMultipartUploadResult {
writeln!(&mut xml, r#"<?xml version="1.0" encoding="UTF-8"?>"#).unwrap(); xmlns: (),
writeln!( location: None,
&mut xml, bucket: s3_xml::Value(bucket),
r#"<CompleteMultipartUploadResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">"# key: s3_xml::Value(key),
) etag: s3_xml::Value(etag),
.unwrap(); };
writeln!( let xml = s3_xml::to_xml_with_header(&result)?;
&mut xml,
"\t<Location>{}</Location>",
garage.config.s3_api.s3_region
)
.unwrap();
writeln!(&mut xml, "\t<Bucket>{}</Bucket>", bucket).unwrap();
writeln!(&mut xml, "\t<Key>{}</Key>", xml_escape(&key)).unwrap();
writeln!(&mut xml, "</CompleteMultipartUploadResult>").unwrap();
Ok(Response::new(Body::from(xml.into_bytes()))) Ok(Response::new(Body::from(xml.into_bytes())))
} }
@@ -557,7 +543,7 @@ pub async fn handle_abort_multipart_upload(
.object_table .object_table
.get(&bucket.to_string(), &key.to_string()) .get(&bucket.to_string(), &key.to_string())
.await?; .await?;
let object = object.ok_or(Error::BadRequest(format!("Object not found")))?; let object = object.ok_or_else(|| Error::BadRequest("Object not found".to_string()))?;
let object_version = object let object_version = object
.versions() .versions()
@@ -629,14 +615,14 @@ pub(crate) fn get_headers(req: &Request<Body>) -> Result<ObjectVersionHeaders, E
}) })
} }
fn decode_upload_id(id: &str) -> Result<UUID, Error> { fn decode_upload_id(id: &str) -> Result<Uuid, Error> {
let id_bin = hex::decode(id).ok_or_bad_request("Invalid upload ID")?; let id_bin = hex::decode(id).ok_or_bad_request("Invalid upload ID")?;
if id_bin.len() != 32 { if id_bin.len() != 32 {
return None.ok_or_bad_request("Invalid upload ID"); return None.ok_or_bad_request("Invalid upload ID");
} }
let mut uuid = [0u8; 32]; let mut uuid = [0u8; 32];
uuid.copy_from_slice(&id_bin[..]); uuid.copy_from_slice(&id_bin[..]);
Ok(UUID::from(uuid)) Ok(Uuid::from(uuid))
} }
#[derive(Debug)] #[derive(Debug)]
+629
View File
@@ -0,0 +1,629 @@
use quick_xml::se::to_string;
use serde::{Serialize, Serializer};
use crate::Error as ApiError;
pub fn to_xml_with_header<T: Serialize>(x: &T) -> Result<String, ApiError> {
let mut xml = r#"<?xml version="1.0" encoding="UTF-8"?>"#.to_string();
xml.push_str(&to_string(x)?);
Ok(xml)
}
fn xmlns_tag<S: Serializer>(_v: &(), s: S) -> Result<S::Ok, S::Error> {
s.serialize_str("http://s3.amazonaws.com/doc/2006-03-01/")
}
#[derive(Debug, Serialize, PartialEq)]
pub struct Value(#[serde(rename = "$value")] pub String);
#[derive(Debug, Serialize, PartialEq)]
pub struct IntValue(#[serde(rename = "$value")] pub i64);
#[derive(Debug, Serialize, PartialEq)]
pub struct Bucket {
#[serde(rename = "CreationDate")]
pub creation_date: Value,
#[serde(rename = "Name")]
pub name: Value,
}
#[derive(Debug, Serialize, PartialEq)]
pub struct DisplayName(#[serde(rename = "$value")] pub String);
#[derive(Debug, Serialize, PartialEq)]
pub struct Id(#[serde(rename = "$value")] pub String);
#[derive(Debug, Serialize, PartialEq)]
pub struct Owner {
#[serde(rename = "DisplayName")]
pub display_name: Value,
#[serde(rename = "ID")]
pub id: Value,
}
#[derive(Debug, Serialize, PartialEq)]
pub struct BucketList {
#[serde(rename = "Bucket")]
pub entries: Vec<Bucket>,
}
#[derive(Debug, Serialize, PartialEq)]
pub struct ListAllMyBucketsResult {
#[serde(rename = "Buckets")]
pub buckets: BucketList,
#[serde(rename = "Owner")]
pub owner: Owner,
}
#[derive(Debug, Serialize, PartialEq)]
pub struct LocationConstraint {
#[serde(serialize_with = "xmlns_tag")]
pub xmlns: (),
#[serde(rename = "$value")]
pub region: String,
}
#[derive(Debug, Serialize, PartialEq)]
pub struct Deleted {
#[serde(rename = "Key")]
pub key: Value,
#[serde(rename = "VersionId")]
pub version_id: Value,
#[serde(rename = "DeleteMarkerVersionId")]
pub delete_marker_version_id: Value,
}
#[derive(Debug, Serialize, PartialEq)]
pub struct Error {
#[serde(rename = "Code")]
pub code: Value,
#[serde(rename = "Message")]
pub message: Value,
#[serde(rename = "Resource")]
pub resource: Option<Value>,
#[serde(rename = "Region")]
pub region: Option<Value>,
}
#[derive(Debug, Serialize, PartialEq)]
pub struct DeleteError {
#[serde(rename = "Code")]
pub code: Value,
#[serde(rename = "Key")]
pub key: Option<Value>,
#[serde(rename = "Message")]
pub message: Value,
#[serde(rename = "VersionId")]
pub version_id: Option<Value>,
}
#[derive(Debug, Serialize, PartialEq)]
pub struct DeleteResult {
#[serde(serialize_with = "xmlns_tag")]
pub xmlns: (),
#[serde(rename = "Deleted")]
pub deleted: Vec<Deleted>,
#[serde(rename = "Error")]
pub errors: Vec<DeleteError>,
}
#[derive(Debug, Serialize, PartialEq)]
pub struct CopyObjectResult {
#[serde(rename = "LastModified")]
pub last_modified: Value,
#[serde(rename = "ETag")]
pub etag: Value,
}
#[derive(Debug, Serialize, PartialEq)]
pub struct InitiateMultipartUploadResult {
#[serde(serialize_with = "xmlns_tag")]
pub xmlns: (),
#[serde(rename = "Bucket")]
pub bucket: Value,
#[serde(rename = "Key")]
pub key: Value,
#[serde(rename = "UploadId")]
pub upload_id: Value,
}
#[derive(Debug, Serialize, PartialEq)]
pub struct CompleteMultipartUploadResult {
#[serde(serialize_with = "xmlns_tag")]
pub xmlns: (),
#[serde(rename = "Location")]
pub location: Option<Value>,
#[serde(rename = "Bucket")]
pub bucket: Value,
#[serde(rename = "Key")]
pub key: Value,
#[serde(rename = "ETag")]
pub etag: Value,
}
#[derive(Debug, Serialize, PartialEq)]
pub struct ListBucketItem {
#[serde(rename = "Key")]
pub key: Value,
#[serde(rename = "LastModified")]
pub last_modified: Value,
#[serde(rename = "ETag")]
pub etag: Value,
#[serde(rename = "Size")]
pub size: IntValue,
#[serde(rename = "StorageClass")]
pub storage_class: Value,
}
#[derive(Debug, Serialize, PartialEq)]
pub struct CommonPrefix {
#[serde(rename = "Prefix")]
pub prefix: Value,
}
#[derive(Debug, Serialize, PartialEq)]
pub struct ListBucketResult {
#[serde(serialize_with = "xmlns_tag")]
pub xmlns: (),
#[serde(rename = "Name")]
pub name: Value,
#[serde(rename = "Prefix")]
pub prefix: Value,
#[serde(rename = "Marker")]
pub marker: Option<Value>,
#[serde(rename = "NextMarker")]
pub next_marker: Option<Value>,
#[serde(rename = "StartAfter")]
pub start_after: Option<Value>,
#[serde(rename = "ContinuationToken")]
pub continuation_token: Option<Value>,
#[serde(rename = "NextContinuationToken")]
pub next_continuation_token: Option<Value>,
#[serde(rename = "KeyCount")]
pub key_count: Option<IntValue>,
#[serde(rename = "MaxKeys")]
pub max_keys: IntValue,
#[serde(rename = "Delimiter")]
pub delimiter: Option<Value>,
#[serde(rename = "EncodingType")]
pub encoding_type: Option<Value>,
#[serde(rename = "IsTruncated")]
pub is_truncated: Value,
#[serde(rename = "Contents")]
pub contents: Vec<ListBucketItem>,
#[serde(rename = "CommonPrefixes")]
pub common_prefixes: Vec<CommonPrefix>,
}
#[derive(Debug, Serialize, PartialEq)]
pub struct VersioningConfiguration {
#[serde(serialize_with = "xmlns_tag")]
pub xmlns: (),
#[serde(rename = "Status")]
pub status: Option<Value>,
}
#[cfg(test)]
mod tests {
use super::*;
use garage_util::time::*;
#[test]
fn error_message() -> Result<(), ApiError> {
let error = Error {
code: Value("TestError".to_string()),
message: Value("A dummy error message".to_string()),
resource: Some(Value("/bucket/a/plop".to_string())),
region: Some(Value("garage".to_string())),
};
assert_eq!(
to_xml_with_header(&error)?,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Error>\
<Code>TestError</Code>\
<Message>A dummy error message</Message>\
<Resource>/bucket/a/plop</Resource>\
<Region>garage</Region>\
</Error>"
);
Ok(())
}
#[test]
fn list_all_my_buckets_result() -> Result<(), ApiError> {
let list_buckets = ListAllMyBucketsResult {
owner: Owner {
display_name: Value("owner_name".to_string()),
id: Value("qsdfjklm".to_string()),
},
buckets: BucketList {
entries: vec![
Bucket {
creation_date: Value(msec_to_rfc3339(0)),
name: Value("bucket_A".to_string()),
},
Bucket {
creation_date: Value(msec_to_rfc3339(3600 * 24 * 1000)),
name: Value("bucket_B".to_string()),
},
],
},
};
assert_eq!(
to_xml_with_header(&list_buckets)?,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<ListAllMyBucketsResult>\
<Buckets>\
<Bucket>\
<CreationDate>1970-01-01T00:00:00.000Z</CreationDate>\
<Name>bucket_A</Name>\
</Bucket>\
<Bucket>\
<CreationDate>1970-01-02T00:00:00.000Z</CreationDate>\
<Name>bucket_B</Name>\
</Bucket>\
</Buckets>\
<Owner>\
<DisplayName>owner_name</DisplayName>\
<ID>qsdfjklm</ID>\
</Owner>\
</ListAllMyBucketsResult>"
);
Ok(())
}
#[test]
fn get_bucket_location_result() -> Result<(), ApiError> {
let get_bucket_location = LocationConstraint {
xmlns: (),
region: "garage".to_string(),
};
assert_eq!(
to_xml_with_header(&get_bucket_location)?,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">garage</LocationConstraint>"
);
Ok(())
}
#[test]
fn get_bucket_versioning_result() -> Result<(), ApiError> {
let get_bucket_versioning = VersioningConfiguration {
xmlns: (),
status: None,
};
assert_eq!(
to_xml_with_header(&get_bucket_versioning)?,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<VersioningConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>"
);
let get_bucket_versioning2 = VersioningConfiguration {
xmlns: (),
status: Some(Value("Suspended".to_string())),
};
assert_eq!(
to_xml_with_header(&get_bucket_versioning2)?,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<VersioningConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Status>Suspended</Status></VersioningConfiguration>"
);
Ok(())
}
#[test]
fn delete_result() -> Result<(), ApiError> {
let delete_result = DeleteResult {
xmlns: (),
deleted: vec![
Deleted {
key: Value("a/plop".to_string()),
version_id: Value("qsdfjklm".to_string()),
delete_marker_version_id: Value("wxcvbn".to_string()),
},
Deleted {
key: Value("b/plip".to_string()),
version_id: Value("1234".to_string()),
delete_marker_version_id: Value("4321".to_string()),
},
],
errors: vec![
DeleteError {
code: Value("NotFound".to_string()),
key: Some(Value("c/plap".to_string())),
message: Value("Object c/plap not found".to_string()),
version_id: None,
},
DeleteError {
code: Value("Forbidden".to_string()),
key: Some(Value("d/plep".to_string())),
message: Value("Not authorized".to_string()),
version_id: Some(Value("789".to_string())),
},
],
};
assert_eq!(
to_xml_with_header(&delete_result)?,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<DeleteResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
<Deleted>\
<Key>a/plop</Key>\
<VersionId>qsdfjklm</VersionId>\
<DeleteMarkerVersionId>wxcvbn</DeleteMarkerVersionId>\
</Deleted>\
<Deleted>\
<Key>b/plip</Key>\
<VersionId>1234</VersionId>\
<DeleteMarkerVersionId>4321</DeleteMarkerVersionId>\
</Deleted>\
<Error>\
<Code>NotFound</Code>\
<Key>c/plap</Key>\
<Message>Object c/plap not found</Message>\
</Error>\
<Error>\
<Code>Forbidden</Code>\
<Key>d/plep</Key>\
<Message>Not authorized</Message>\
<VersionId>789</VersionId>\
</Error>\
</DeleteResult>"
);
Ok(())
}
#[test]
fn copy_object_result() -> Result<(), ApiError> {
let copy_result = CopyObjectResult {
last_modified: Value(msec_to_rfc3339(0)),
etag: Value("9b2cf535f27731c974343645a3985328".to_string()),
};
assert_eq!(
to_xml_with_header(&copy_result)?,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<CopyObjectResult>\
<LastModified>1970-01-01T00:00:00.000Z</LastModified>\
<ETag>9b2cf535f27731c974343645a3985328</ETag>\
</CopyObjectResult>\
"
);
Ok(())
}
#[test]
fn initiate_multipart_upload_result() -> Result<(), ApiError> {
let result = InitiateMultipartUploadResult {
xmlns: (),
bucket: Value("mybucket".to_string()),
key: Value("a/plop".to_string()),
upload_id: Value("azerty".to_string()),
};
assert_eq!(
to_xml_with_header(&result)?,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
<Bucket>mybucket</Bucket>\
<Key>a/plop</Key>\
<UploadId>azerty</UploadId>\
</InitiateMultipartUploadResult>"
);
Ok(())
}
#[test]
fn complete_multipart_upload_result() -> Result<(), ApiError> {
let result = CompleteMultipartUploadResult {
xmlns: (),
location: Some(Value("https://garage.tld/mybucket/a/plop".to_string())),
bucket: Value("mybucket".to_string()),
key: Value("a/plop".to_string()),
etag: Value("3858f62230ac3c915f300c664312c11f-9".to_string()),
};
assert_eq!(
to_xml_with_header(&result)?,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
<Location>https://garage.tld/mybucket/a/plop</Location>\
<Bucket>mybucket</Bucket>\
<Key>a/plop</Key>\
<ETag>3858f62230ac3c915f300c664312c11f-9</ETag>\
</CompleteMultipartUploadResult>"
);
Ok(())
}
#[test]
fn list_objects_v1_1() -> Result<(), ApiError> {
let result = ListBucketResult {
xmlns: (),
name: Value("example-bucket".to_string()),
prefix: Value("".to_string()),
marker: Some(Value("".to_string())),
next_marker: None,
start_after: None,
continuation_token: None,
next_continuation_token: None,
key_count: None,
max_keys: IntValue(1000),
encoding_type: None,
delimiter: Some(Value("/".to_string())),
is_truncated: Value("false".to_string()),
contents: vec![ListBucketItem {
key: Value("sample.jpg".to_string()),
last_modified: Value(msec_to_rfc3339(0)),
etag: Value("bf1d737a4d46a19f3bced6905cc8b902".to_string()),
size: IntValue(142863),
storage_class: Value("STANDARD".to_string()),
}],
common_prefixes: vec![CommonPrefix {
prefix: Value("photos/".to_string()),
}],
};
assert_eq!(
to_xml_with_header(&result)?,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
<Name>example-bucket</Name>\
<Prefix></Prefix>\
<Marker></Marker>\
<MaxKeys>1000</MaxKeys>\
<Delimiter>/</Delimiter>\
<IsTruncated>false</IsTruncated>\
<Contents>\
<Key>sample.jpg</Key>\
<LastModified>1970-01-01T00:00:00.000Z</LastModified>\
<ETag>bf1d737a4d46a19f3bced6905cc8b902</ETag>\
<Size>142863</Size>\
<StorageClass>STANDARD</StorageClass>\
</Contents>\
<CommonPrefixes>\
<Prefix>photos/</Prefix>\
</CommonPrefixes>\
</ListBucketResult>"
);
Ok(())
}
#[test]
fn list_objects_v1_2() -> Result<(), ApiError> {
let result = ListBucketResult {
xmlns: (),
name: Value("example-bucket".to_string()),
prefix: Value("photos/2006/".to_string()),
marker: Some(Value("".to_string())),
next_marker: None,
start_after: None,
continuation_token: None,
next_continuation_token: None,
key_count: None,
max_keys: IntValue(1000),
delimiter: Some(Value("/".to_string())),
encoding_type: None,
is_truncated: Value("false".to_string()),
contents: vec![],
common_prefixes: vec![
CommonPrefix {
prefix: Value("photos/2006/February/".to_string()),
},
CommonPrefix {
prefix: Value("photos/2006/January/".to_string()),
},
],
};
assert_eq!(
to_xml_with_header(&result)?,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
<Name>example-bucket</Name>\
<Prefix>photos/2006/</Prefix>\
<Marker></Marker>\
<MaxKeys>1000</MaxKeys>\
<Delimiter>/</Delimiter>\
<IsTruncated>false</IsTruncated>\
<CommonPrefixes>\
<Prefix>photos/2006/February/</Prefix>\
</CommonPrefixes>\
<CommonPrefixes>\
<Prefix>photos/2006/January/</Prefix>\
</CommonPrefixes>\
</ListBucketResult>"
);
Ok(())
}
#[test]
fn list_objects_v2_1() -> Result<(), ApiError> {
let result = ListBucketResult {
xmlns: (),
name: Value("quotes".to_string()),
prefix: Value("E".to_string()),
marker: None,
next_marker: None,
start_after: Some(Value("ExampleGuide.pdf".to_string())),
continuation_token: None,
next_continuation_token: None,
key_count: None,
max_keys: IntValue(3),
delimiter: None,
encoding_type: None,
is_truncated: Value("false".to_string()),
contents: vec![ListBucketItem {
key: Value("ExampleObject.txt".to_string()),
last_modified: Value(msec_to_rfc3339(0)),
etag: Value("599bab3ed2c697f1d26842727561fd94".to_string()),
size: IntValue(857),
storage_class: Value("REDUCED_REDUNDANCY".to_string()),
}],
common_prefixes: vec![],
};
assert_eq!(
to_xml_with_header(&result)?,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
<Name>quotes</Name>\
<Prefix>E</Prefix>\
<StartAfter>ExampleGuide.pdf</StartAfter>\
<MaxKeys>3</MaxKeys>\
<IsTruncated>false</IsTruncated>\
<Contents>\
<Key>ExampleObject.txt</Key>\
<LastModified>1970-01-01T00:00:00.000Z</LastModified>\
<ETag>599bab3ed2c697f1d26842727561fd94</ETag>\
<Size>857</Size>\
<StorageClass>REDUCED_REDUNDANCY</StorageClass>\
</Contents>\
</ListBucketResult>"
);
Ok(())
}
#[test]
fn list_objects_v2_2() -> Result<(), ApiError> {
let result = ListBucketResult {
xmlns: (),
name: Value("bucket".to_string()),
prefix: Value("".to_string()),
marker: None,
next_marker: None,
start_after: None,
continuation_token: Some(Value(
"1ueGcxLPRx1Tr/XYExHnhbYLgveDs2J/wm36Hy4vbOwM=".to_string(),
)),
next_continuation_token: Some(Value("qsdfjklm".to_string())),
key_count: Some(IntValue(112)),
max_keys: IntValue(1000),
delimiter: None,
encoding_type: None,
is_truncated: Value("false".to_string()),
contents: vec![ListBucketItem {
key: Value("happyfacex.jpg".to_string()),
last_modified: Value(msec_to_rfc3339(0)),
etag: Value("70ee1738b6b21e2c8a43f3a5ab0eee71".to_string()),
size: IntValue(1111),
storage_class: Value("STANDARD".to_string()),
}],
common_prefixes: vec![],
};
assert_eq!(
to_xml_with_header(&result)?,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
<Name>bucket</Name>\
<Prefix></Prefix>\
<ContinuationToken>1ueGcxLPRx1Tr/XYExHnhbYLgveDs2J/wm36Hy4vbOwM=</ContinuationToken>\
<NextContinuationToken>qsdfjklm</NextContinuationToken>\
<KeyCount>112</KeyCount>\
<MaxKeys>1000</MaxKeys>\
<IsTruncated>false</IsTruncated>\
<Contents>\
<Key>happyfacex.jpg</Key>\
<LastModified>1970-01-01T00:00:00.000Z</LastModified>\
<ETag>70ee1738b6b21e2c8a43f3a5ab0eee71</ETag>\
<Size>1111</Size>\
<StorageClass>STANDARD</StorageClass>\
</Contents>\
</ListBucketResult>"
);
Ok(())
}
}
+13 -19
View File
@@ -43,13 +43,12 @@ pub async fn check_signature(
let date = headers let date = headers
.get("x-amz-date") .get("x-amz-date")
.ok_or_bad_request("Missing X-Amz-Date field")?; .ok_or_bad_request("Missing X-Amz-Date field")?;
let date: NaiveDateTime = NaiveDateTime::parse_from_str(date, LONG_DATETIME) let date: NaiveDateTime =
.ok_or_bad_request("Invalid date")? NaiveDateTime::parse_from_str(date, LONG_DATETIME).ok_or_bad_request("Invalid date")?;
.into();
let date: DateTime<Utc> = DateTime::from_utc(date, Utc); let date: DateTime<Utc> = DateTime::from_utc(date, Utc);
if Utc::now() - date > Duration::hours(24) { if Utc::now() - date > Duration::hours(24) {
return Err(Error::BadRequest(format!("Date is too old"))); return Err(Error::BadRequest("Date is too old".to_string()));
} }
let scope = format!( let scope = format!(
@@ -66,10 +65,7 @@ pub async fn check_signature(
.get(&EmptyKey, &authorization.key_id) .get(&EmptyKey, &authorization.key_id)
.await? .await?
.filter(|k| !k.deleted.get()) .filter(|k| !k.deleted.get())
.ok_or(Error::Forbidden(format!( .ok_or_else(|| Error::Forbidden(format!("No such key: {}", authorization.key_id)))?;
"No such key: {}",
authorization.key_id
)))?;
let canonical_request = canonical_request( let canonical_request = canonical_request(
request.method(), request.method(),
@@ -95,19 +91,17 @@ pub async fn check_signature(
trace!("Canonical request: ``{}``", canonical_request); trace!("Canonical request: ``{}``", canonical_request);
trace!("String to sign: ``{}``", string_to_sign); trace!("String to sign: ``{}``", string_to_sign);
trace!("Expected: {}, got: {}", signature, authorization.signature); trace!("Expected: {}, got: {}", signature, authorization.signature);
return Err(Error::Forbidden(format!("Invalid signature"))); return Err(Error::Forbidden("Invalid signature".to_string()));
} }
let content_sha256 = if authorization.content_sha256 == "UNSIGNED-PAYLOAD" let content_sha256 = if authorization.content_sha256 == "UNSIGNED-PAYLOAD" {
|| authorization.content_sha256 == "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
{
None None
} else { } else {
let bytes = hex::decode(authorization.content_sha256) let bytes = hex::decode(authorization.content_sha256)
.ok_or_bad_request("Invalid content sha256 hash")?; .ok_or_bad_request("Invalid content sha256 hash")?;
Some( Some(
Hash::try_from(&bytes[..]) Hash::try_from(&bytes[..])
.ok_or(Error::BadRequest(format!("Invalid content sha256 hash")))?, .ok_or_else(|| Error::BadRequest("Invalid content sha256 hash".to_string()))?,
) )
}; };
@@ -175,9 +169,9 @@ fn parse_query_authorization(headers: &HashMap<String, String>) -> Result<Author
.get("x-amz-algorithm") .get("x-amz-algorithm")
.ok_or_bad_request("X-Amz-Algorithm not found in query parameters")?; .ok_or_bad_request("X-Amz-Algorithm not found in query parameters")?;
if algo != "AWS4-HMAC-SHA256" { if algo != "AWS4-HMAC-SHA256" {
return Err(Error::BadRequest(format!( return Err(Error::BadRequest(
"Unsupported authorization method" "Unsupported authorization method".to_string(),
))); ));
} }
let cred = headers let cred = headers
@@ -295,9 +289,9 @@ pub fn verify_signed_content(content_sha256: Option<Hash>, body: &[u8]) -> Resul
let expected_sha256 = let expected_sha256 =
content_sha256.ok_or_bad_request("Request content hash not signed, aborting.")?; content_sha256.ok_or_bad_request("Request content hash not signed, aborting.")?;
if expected_sha256 != sha256sum(body) { if expected_sha256 != sha256sum(body) {
return Err(Error::BadRequest(format!( return Err(Error::BadRequest(
"Request content hash does not match signed hash" "Request content hash does not match signed hash".to_string(),
))); ));
} }
Ok(()) Ok(())
} }
+7 -7
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage" name = "garage"
version = "0.2.1" version = "0.3.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
@@ -14,12 +14,12 @@ path = "main.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
garage_api = { version = "0.2.1", path = "../api" } garage_api = { version = "0.3.0", path = "../api" }
garage_model = { version = "0.2.1", path = "../model" } garage_model = { version = "0.3.0", path = "../model" }
garage_rpc = { version = "0.2.1", path = "../rpc" } garage_rpc = { version = "0.3.0", path = "../rpc" }
garage_table = { version = "0.2.1", path = "../table" } garage_table = { version = "0.3.0", path = "../table" }
garage_util = { version = "0.2.1", path = "../util" } garage_util = { version = "0.3.0", path = "../util" }
garage_web = { version = "0.2.1", path = "../web" } garage_web = { version = "0.3.0", path = "../web" }
bytes = "1.0" bytes = "1.0"
git-version = "0.3.4" git-version = "0.3.4"
+68 -76
View File
@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use garage_util::error::Error; use garage_util::error::Error;
use garage_table::crdt::CRDT; use garage_table::crdt::Crdt;
use garage_table::replication::*; use garage_table::replication::*;
use garage_table::*; use garage_table::*;
@@ -25,7 +25,7 @@ pub const ADMIN_RPC_TIMEOUT: Duration = Duration::from_secs(30);
pub const ADMIN_RPC_PATH: &str = "_admin"; pub const ADMIN_RPC_PATH: &str = "_admin";
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum AdminRPC { pub enum AdminRpc {
BucketOperation(BucketOperation), BucketOperation(BucketOperation),
KeyOperation(KeyOperation), KeyOperation(KeyOperation),
LaunchRepair(RepairOpt), LaunchRepair(RepairOpt),
@@ -39,35 +39,35 @@ pub enum AdminRPC {
KeyInfo(Key), KeyInfo(Key),
} }
impl RpcMessage for AdminRPC {} impl RpcMessage for AdminRpc {}
pub struct AdminRpcHandler { pub struct AdminRpcHandler {
garage: Arc<Garage>, garage: Arc<Garage>,
rpc_client: Arc<RpcClient<AdminRPC>>, rpc_client: Arc<RpcClient<AdminRpc>>,
} }
impl AdminRpcHandler { impl AdminRpcHandler {
pub fn new(garage: Arc<Garage>) -> Arc<Self> { pub fn new(garage: Arc<Garage>) -> Arc<Self> {
let rpc_client = garage.system.clone().rpc_client::<AdminRPC>(ADMIN_RPC_PATH); let rpc_client = garage.system.clone().rpc_client::<AdminRpc>(ADMIN_RPC_PATH);
Arc::new(Self { garage, rpc_client }) Arc::new(Self { garage, rpc_client })
} }
pub fn register_handler(self: Arc<Self>, rpc_server: &mut RpcServer) { pub fn register_handler(self: Arc<Self>, rpc_server: &mut RpcServer) {
rpc_server.add_handler::<AdminRPC, _, _>(ADMIN_RPC_PATH.to_string(), move |msg, _addr| { rpc_server.add_handler::<AdminRpc, _, _>(ADMIN_RPC_PATH.to_string(), move |msg, _addr| {
let self2 = self.clone(); let self2 = self.clone();
async move { async move {
match msg { match msg {
AdminRPC::BucketOperation(bo) => self2.handle_bucket_cmd(bo).await, AdminRpc::BucketOperation(bo) => self2.handle_bucket_cmd(bo).await,
AdminRPC::KeyOperation(ko) => self2.handle_key_cmd(ko).await, AdminRpc::KeyOperation(ko) => self2.handle_key_cmd(ko).await,
AdminRPC::LaunchRepair(opt) => self2.handle_launch_repair(opt).await, AdminRpc::LaunchRepair(opt) => self2.handle_launch_repair(opt).await,
AdminRPC::Stats(opt) => self2.handle_stats(opt).await, AdminRpc::Stats(opt) => self2.handle_stats(opt).await,
_ => Err(Error::BadRPC(format!("Invalid RPC"))), _ => Err(Error::BadRpc("Invalid RPC".to_string())),
} }
} }
}); });
} }
async fn handle_bucket_cmd(&self, cmd: BucketOperation) -> Result<AdminRPC, Error> { async fn handle_bucket_cmd(&self, cmd: BucketOperation) -> Result<AdminRpc, Error> {
match cmd { match cmd {
BucketOperation::List => { BucketOperation::List => {
let bucket_names = self let bucket_names = self
@@ -78,17 +78,17 @@ impl AdminRpcHandler {
.iter() .iter()
.map(|b| b.name.to_string()) .map(|b| b.name.to_string())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Ok(AdminRPC::BucketList(bucket_names)) Ok(AdminRpc::BucketList(bucket_names))
} }
BucketOperation::Info(query) => { BucketOperation::Info(query) => {
let bucket = self.get_existing_bucket(&query.name).await?; let bucket = self.get_existing_bucket(&query.name).await?;
Ok(AdminRPC::BucketInfo(bucket)) Ok(AdminRpc::BucketInfo(bucket))
} }
BucketOperation::Create(query) => { BucketOperation::Create(query) => {
let bucket = match self.garage.bucket_table.get(&EmptyKey, &query.name).await? { let bucket = match self.garage.bucket_table.get(&EmptyKey, &query.name).await? {
Some(mut bucket) => { Some(mut bucket) => {
if !bucket.is_deleted() { if !bucket.is_deleted() {
return Err(Error::BadRPC(format!( return Err(Error::BadRpc(format!(
"Bucket {} already exists", "Bucket {} already exists",
query.name query.name
))); )));
@@ -101,7 +101,7 @@ impl AdminRpcHandler {
None => Bucket::new(query.name.clone()), None => Bucket::new(query.name.clone()),
}; };
self.garage.bucket_table.insert(&bucket).await?; self.garage.bucket_table.insert(&bucket).await?;
Ok(AdminRPC::Ok(format!("Bucket {} was created.", query.name))) Ok(AdminRpc::Ok(format!("Bucket {} was created.", query.name)))
} }
BucketOperation::Delete(query) => { BucketOperation::Delete(query) => {
let mut bucket = self.get_existing_bucket(&query.name).await?; let mut bucket = self.get_existing_bucket(&query.name).await?;
@@ -111,12 +111,12 @@ impl AdminRpcHandler {
.get_range(&query.name, None, Some(DeletedFilter::NotDeleted), 10) .get_range(&query.name, None, Some(DeletedFilter::NotDeleted), 10)
.await?; .await?;
if !objects.is_empty() { if !objects.is_empty() {
return Err(Error::BadRPC(format!("Bucket {} is not empty", query.name))); return Err(Error::BadRpc(format!("Bucket {} is not empty", query.name)));
} }
if !query.yes { if !query.yes {
return Err(Error::BadRPC(format!( return Err(Error::BadRpc(
"Add --yes flag to really perform this operation" "Add --yes flag to really perform this operation".to_string(),
))); ));
} }
// --- done checking, now commit --- // --- done checking, now commit ---
for (key_id, _, _) in bucket.authorized_keys() { for (key_id, _, _) in bucket.authorized_keys() {
@@ -131,7 +131,7 @@ impl AdminRpcHandler {
} }
bucket.state.update(BucketState::Deleted); bucket.state.update(BucketState::Deleted);
self.garage.bucket_table.insert(&bucket).await?; self.garage.bucket_table.insert(&bucket).await?;
Ok(AdminRPC::Ok(format!("Bucket {} was deleted.", query.name))) Ok(AdminRpc::Ok(format!("Bucket {} was deleted.", query.name)))
} }
BucketOperation::Allow(query) => { BucketOperation::Allow(query) => {
let key = self.get_existing_key(&query.key_pattern).await?; let key = self.get_existing_key(&query.key_pattern).await?;
@@ -142,7 +142,7 @@ impl AdminRpcHandler {
.await?; .await?;
self.update_bucket_key(bucket, &key.key_id, allow_read, allow_write) self.update_bucket_key(bucket, &key.key_id, allow_read, allow_write)
.await?; .await?;
Ok(AdminRPC::Ok(format!( Ok(AdminRpc::Ok(format!(
"New permissions for {} on {}: read {}, write {}.", "New permissions for {} on {}: read {}, write {}.",
&key.key_id, &query.bucket, allow_read, allow_write &key.key_id, &query.bucket, allow_read, allow_write
))) )))
@@ -156,7 +156,7 @@ impl AdminRpcHandler {
.await?; .await?;
self.update_bucket_key(bucket, &key.key_id, allow_read, allow_write) self.update_bucket_key(bucket, &key.key_id, allow_read, allow_write)
.await?; .await?;
Ok(AdminRPC::Ok(format!( Ok(AdminRpc::Ok(format!(
"New permissions for {} on {}: read {}, write {}.", "New permissions for {} on {}: read {}, write {}.",
&key.key_id, &query.bucket, allow_read, allow_write &key.key_id, &query.bucket, allow_read, allow_write
))) )))
@@ -165,9 +165,9 @@ impl AdminRpcHandler {
let mut bucket = self.get_existing_bucket(&query.bucket).await?; let mut bucket = self.get_existing_bucket(&query.bucket).await?;
if !(query.allow ^ query.deny) { if !(query.allow ^ query.deny) {
return Err(Error::Message(format!( return Err(Error::Message(
"You must specify exactly one flag, either --allow or --deny" "You must specify exactly one flag, either --allow or --deny".to_string(),
))); ));
} }
if let BucketState::Present(state) = bucket.state.get_mut() { if let BucketState::Present(state) = bucket.state.get_mut() {
@@ -179,7 +179,7 @@ impl AdminRpcHandler {
format!("Website access denied for {}", &query.bucket) format!("Website access denied for {}", &query.bucket)
}; };
Ok(AdminRPC::Ok(msg.to_string())) Ok(AdminRpc::Ok(msg))
} else { } else {
unreachable!(); unreachable!();
} }
@@ -187,7 +187,7 @@ impl AdminRpcHandler {
} }
} }
async fn handle_key_cmd(&self, cmd: KeyOperation) -> Result<AdminRPC, Error> { async fn handle_key_cmd(&self, cmd: KeyOperation) -> Result<AdminRpc, Error> {
match cmd { match cmd {
KeyOperation::List => { KeyOperation::List => {
let key_ids = self let key_ids = self
@@ -203,29 +203,29 @@ impl AdminRpcHandler {
.iter() .iter()
.map(|k| (k.key_id.to_string(), k.name.get().clone())) .map(|k| (k.key_id.to_string(), k.name.get().clone()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Ok(AdminRPC::KeyList(key_ids)) Ok(AdminRpc::KeyList(key_ids))
} }
KeyOperation::Info(query) => { KeyOperation::Info(query) => {
let key = self.get_existing_key(&query.key_pattern).await?; let key = self.get_existing_key(&query.key_pattern).await?;
Ok(AdminRPC::KeyInfo(key)) Ok(AdminRpc::KeyInfo(key))
} }
KeyOperation::New(query) => { KeyOperation::New(query) => {
let key = Key::new(query.name); let key = Key::new(query.name);
self.garage.key_table.insert(&key).await?; self.garage.key_table.insert(&key).await?;
Ok(AdminRPC::KeyInfo(key)) Ok(AdminRpc::KeyInfo(key))
} }
KeyOperation::Rename(query) => { KeyOperation::Rename(query) => {
let mut key = self.get_existing_key(&query.key_pattern).await?; let mut key = self.get_existing_key(&query.key_pattern).await?;
key.name.update(query.new_name); key.name.update(query.new_name);
self.garage.key_table.insert(&key).await?; self.garage.key_table.insert(&key).await?;
Ok(AdminRPC::KeyInfo(key)) Ok(AdminRpc::KeyInfo(key))
} }
KeyOperation::Delete(query) => { KeyOperation::Delete(query) => {
let key = self.get_existing_key(&query.key_pattern).await?; let key = self.get_existing_key(&query.key_pattern).await?;
if !query.yes { if !query.yes {
return Err(Error::BadRPC(format!( return Err(Error::BadRpc(
"Add --yes flag to really perform this operation" "Add --yes flag to really perform this operation".to_string(),
))); ));
} }
// --- done checking, now commit --- // --- done checking, now commit ---
for (ab_name, _, _) in key.authorized_buckets.items().iter() { for (ab_name, _, _) in key.authorized_buckets.items().iter() {
@@ -240,7 +240,7 @@ impl AdminRpcHandler {
} }
let del_key = Key::delete(key.key_id.to_string()); let del_key = Key::delete(key.key_id.to_string());
self.garage.key_table.insert(&del_key).await?; self.garage.key_table.insert(&del_key).await?;
Ok(AdminRPC::Ok(format!( Ok(AdminRpc::Ok(format!(
"Key {} was deleted successfully.", "Key {} was deleted successfully.",
key.key_id key.key_id
))) )))
@@ -252,11 +252,12 @@ impl AdminRpcHandler {
} }
let imported_key = Key::import(&query.key_id, &query.secret_key, &query.name); let imported_key = Key::import(&query.key_id, &query.secret_key, &query.name);
self.garage.key_table.insert(&imported_key).await?; self.garage.key_table.insert(&imported_key).await?;
Ok(AdminRPC::KeyInfo(imported_key)) Ok(AdminRpc::KeyInfo(imported_key))
} }
} }
} }
#[allow(clippy::ptr_arg)]
async fn get_existing_bucket(&self, bucket: &String) -> Result<Bucket, Error> { async fn get_existing_bucket(&self, bucket: &String) -> Result<Bucket, Error> {
self.garage self.garage
.bucket_table .bucket_table
@@ -264,10 +265,7 @@ impl AdminRpcHandler {
.await? .await?
.filter(|b| !b.is_deleted()) .filter(|b| !b.is_deleted())
.map(Ok) .map(Ok)
.unwrap_or(Err(Error::BadRPC(format!( .unwrap_or_else(|| Err(Error::BadRpc(format!("Bucket {} does not exist", bucket))))
"Bucket {} does not exist",
bucket
))))
} }
async fn get_existing_key(&self, pattern: &str) -> Result<Key, Error> { async fn get_existing_key(&self, pattern: &str) -> Result<Key, Error> {
@@ -298,7 +296,7 @@ impl AdminRpcHandler {
async fn update_bucket_key( async fn update_bucket_key(
&self, &self,
mut bucket: Bucket, mut bucket: Bucket,
key_id: &String, key_id: &str,
allow_read: bool, allow_read: bool,
allow_write: bool, allow_write: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
@@ -313,9 +311,9 @@ impl AdminRpcHandler {
}, },
)); ));
} else { } else {
return Err(Error::Message(format!( return Err(Error::Message(
"Bucket is deleted in update_bucket_key" "Bucket is deleted in update_bucket_key".to_string(),
))); ));
} }
self.garage.bucket_table.insert(&bucket).await?; self.garage.bucket_table.insert(&bucket).await?;
Ok(()) Ok(())
@@ -325,14 +323,14 @@ impl AdminRpcHandler {
async fn update_key_bucket( async fn update_key_bucket(
&self, &self,
key: &Key, key: &Key,
bucket: &String, bucket: &str,
allow_read: bool, allow_read: bool,
allow_write: bool, allow_write: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut key = key.clone(); let mut key = key.clone();
let old_map = key.authorized_buckets.take_and_clear(); let old_map = key.authorized_buckets.take_and_clear();
key.authorized_buckets.merge(&old_map.update_mutator( key.authorized_buckets.merge(&old_map.update_mutator(
bucket.clone(), bucket.to_string(),
PermissionSet { PermissionSet {
allow_read, allow_read,
allow_write, allow_write,
@@ -342,11 +340,11 @@ impl AdminRpcHandler {
Ok(()) Ok(())
} }
async fn handle_launch_repair(self: &Arc<Self>, opt: RepairOpt) -> Result<AdminRPC, Error> { async fn handle_launch_repair(self: &Arc<Self>, opt: RepairOpt) -> Result<AdminRpc, Error> {
if !opt.yes { if !opt.yes {
return Err(Error::BadRPC(format!( return Err(Error::BadRpc(
"Please provide the --yes flag to initiate repair operations." "Please provide the --yes flag to initiate repair operations.".to_string(),
))); ));
} }
if opt.all_nodes { if opt.all_nodes {
let mut opt_to_send = opt.clone(); let mut opt_to_send = opt.clone();
@@ -359,17 +357,17 @@ impl AdminRpcHandler {
.rpc_client .rpc_client
.call( .call(
*node, *node,
AdminRPC::LaunchRepair(opt_to_send.clone()), AdminRpc::LaunchRepair(opt_to_send.clone()),
ADMIN_RPC_TIMEOUT, ADMIN_RPC_TIMEOUT,
) )
.await .await
.is_err() .is_err()
{ {
failures.push(node.clone()); failures.push(*node);
} }
} }
if failures.is_empty() { if failures.is_empty() {
Ok(AdminRPC::Ok(format!("Repair launched on all nodes"))) Ok(AdminRpc::Ok("Repair launched on all nodes".to_string()))
} else { } else {
Err(Error::Message(format!( Err(Error::Message(format!(
"Could not launch repair on nodes: {:?} (launched successfully on other nodes)", "Could not launch repair on nodes: {:?} (launched successfully on other nodes)",
@@ -386,14 +384,14 @@ impl AdminRpcHandler {
.spawn_worker("Repair worker".into(), move |must_exit| async move { .spawn_worker("Repair worker".into(), move |must_exit| async move {
repair.repair_worker(opt, must_exit).await repair.repair_worker(opt, must_exit).await
}); });
Ok(AdminRPC::Ok(format!( Ok(AdminRpc::Ok(format!(
"Repair launched on {:?}", "Repair launched on {:?}",
self.garage.system.id self.garage.system.id
))) )))
} }
} }
async fn handle_stats(&self, opt: StatsOpt) -> Result<AdminRPC, Error> { async fn handle_stats(&self, opt: StatsOpt) -> Result<AdminRpc, Error> {
if opt.all_nodes { if opt.all_nodes {
let mut ret = String::new(); let mut ret = String::new();
let ring = self.garage.system.ring.borrow().clone(); let ring = self.garage.system.ring.borrow().clone();
@@ -406,21 +404,21 @@ impl AdminRpcHandler {
writeln!(&mut ret, "Stats for node {:?}:", node).unwrap(); writeln!(&mut ret, "Stats for node {:?}:", node).unwrap();
match self match self
.rpc_client .rpc_client
.call(*node, AdminRPC::Stats(opt), ADMIN_RPC_TIMEOUT) .call(*node, AdminRpc::Stats(opt), ADMIN_RPC_TIMEOUT)
.await .await
{ {
Ok(AdminRPC::Ok(s)) => writeln!(&mut ret, "{}", s).unwrap(), Ok(AdminRpc::Ok(s)) => writeln!(&mut ret, "{}", s).unwrap(),
Ok(x) => writeln!(&mut ret, "Bad answer: {:?}", x).unwrap(), Ok(x) => writeln!(&mut ret, "Bad answer: {:?}", x).unwrap(),
Err(e) => writeln!(&mut ret, "Error: {}", e).unwrap(), Err(e) => writeln!(&mut ret, "Error: {}", e).unwrap(),
} }
} }
Ok(AdminRPC::Ok(ret)) Ok(AdminRpc::Ok(ret))
} else { } else {
Ok(AdminRPC::Ok(self.gather_stats_local(opt)?)) Ok(AdminRpc::Ok(self.gather_stats_local(opt)))
} }
} }
fn gather_stats_local(&self, opt: StatsOpt) -> Result<String, Error> { fn gather_stats_local(&self, opt: StatsOpt) -> String {
let mut ret = String::new(); let mut ret = String::new();
writeln!( writeln!(
&mut ret, &mut ret,
@@ -432,8 +430,8 @@ impl AdminRpcHandler {
// Gather ring statistics // Gather ring statistics
let ring = self.garage.system.ring.borrow().clone(); let ring = self.garage.system.ring.borrow().clone();
let mut ring_nodes = HashMap::new(); let mut ring_nodes = HashMap::new();
for r in ring.ring.iter() { for (_i, loc) in ring.partitions().iter() {
for n in r.nodes.iter() { for n in ring.get_nodes(loc, ring.replication_factor).iter() {
if !ring_nodes.contains_key(n) { if !ring_nodes.contains_key(n) {
ring_nodes.insert(*n, 0usize); ring_nodes.insert(*n, 0usize);
} }
@@ -445,11 +443,11 @@ impl AdminRpcHandler {
writeln!(&mut ret, " {:?} {}", n, c).unwrap(); writeln!(&mut ret, " {:?} {}", n, c).unwrap();
} }
self.gather_table_stats(&mut ret, &self.garage.bucket_table, &opt)?; self.gather_table_stats(&mut ret, &self.garage.bucket_table, &opt);
self.gather_table_stats(&mut ret, &self.garage.key_table, &opt)?; self.gather_table_stats(&mut ret, &self.garage.key_table, &opt);
self.gather_table_stats(&mut ret, &self.garage.object_table, &opt)?; self.gather_table_stats(&mut ret, &self.garage.object_table, &opt);
self.gather_table_stats(&mut ret, &self.garage.version_table, &opt)?; self.gather_table_stats(&mut ret, &self.garage.version_table, &opt);
self.gather_table_stats(&mut ret, &self.garage.block_ref_table, &opt)?; self.gather_table_stats(&mut ret, &self.garage.block_ref_table, &opt);
writeln!(&mut ret, "\nBlock manager stats:").unwrap(); writeln!(&mut ret, "\nBlock manager stats:").unwrap();
if opt.detailed { if opt.detailed {
@@ -467,15 +465,10 @@ impl AdminRpcHandler {
) )
.unwrap(); .unwrap();
Ok(ret) ret
} }
fn gather_table_stats<F, R>( fn gather_table_stats<F, R>(&self, to: &mut String, t: &Arc<Table<F, R>>, opt: &StatsOpt)
&self,
to: &mut String,
t: &Arc<Table<F, R>>,
opt: &StatsOpt,
) -> Result<(), Error>
where where
F: TableSchema + 'static, F: TableSchema + 'static,
R: TableReplication + 'static, R: TableReplication + 'static,
@@ -497,6 +490,5 @@ impl AdminRpcHandler {
) )
.unwrap(); .unwrap();
writeln!(to, " GC todo queue length: {}", t.data.gc_todo_len()).unwrap(); writeln!(to, " GC todo queue length: {}", t.data.gc_todo_len()).unwrap();
Ok(())
} }
} }
+67 -36
View File
@@ -5,7 +5,7 @@ use std::path::PathBuf;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use structopt::StructOpt; use structopt::StructOpt;
use garage_util::data::UUID; use garage_util::data::Uuid;
use garage_util::error::Error; use garage_util::error::Error;
use garage_util::time::*; use garage_util::time::*;
@@ -72,14 +72,18 @@ pub struct ConfigureNodeOpt {
/// Node to configure (prefix of hexadecimal node id) /// Node to configure (prefix of hexadecimal node id)
node_id: String, node_id: String,
/// Location (datacenter) of the node /// Location (zone or datacenter) of the node
#[structopt(short = "d", long = "datacenter")] #[structopt(short = "z", long = "zone")]
datacenter: Option<String>, zone: Option<String>,
/// Capacity (in relative terms, use 1 to represent your smallest server) /// Capacity (in relative terms, use 1 to represent your smallest server)
#[structopt(short = "c", long = "capacity")] #[structopt(short = "c", long = "capacity")]
capacity: Option<u32>, capacity: Option<u32>,
/// Gateway-only node
#[structopt(short = "g", long = "gateway")]
gateway: bool,
/// Optional node tag /// Optional node tag
#[structopt(short = "t", long = "tag")] #[structopt(short = "t", long = "tag")]
tag: Option<String>, tag: Option<String>,
@@ -294,7 +298,7 @@ pub struct StatsOpt {
pub async fn cli_cmd( pub async fn cli_cmd(
cmd: Command, cmd: Command,
membership_rpc_cli: RpcAddrClient<Message>, membership_rpc_cli: RpcAddrClient<Message>,
admin_rpc_cli: RpcAddrClient<AdminRPC>, admin_rpc_cli: RpcAddrClient<AdminRpc>,
rpc_host: SocketAddr, rpc_host: SocketAddr,
) -> Result<(), Error> { ) -> Result<(), Error> {
match cmd { match cmd {
@@ -306,11 +310,11 @@ pub async fn cli_cmd(
cmd_remove(membership_rpc_cli, rpc_host, remove_opt).await cmd_remove(membership_rpc_cli, rpc_host, remove_opt).await
} }
Command::Bucket(bo) => { Command::Bucket(bo) => {
cmd_admin(admin_rpc_cli, rpc_host, AdminRPC::BucketOperation(bo)).await cmd_admin(admin_rpc_cli, rpc_host, AdminRpc::BucketOperation(bo)).await
} }
Command::Key(ko) => cmd_admin(admin_rpc_cli, rpc_host, AdminRPC::KeyOperation(ko)).await, Command::Key(ko) => cmd_admin(admin_rpc_cli, rpc_host, AdminRpc::KeyOperation(ko)).await,
Command::Repair(ro) => cmd_admin(admin_rpc_cli, rpc_host, AdminRPC::LaunchRepair(ro)).await, Command::Repair(ro) => cmd_admin(admin_rpc_cli, rpc_host, AdminRpc::LaunchRepair(ro)).await,
Command::Stats(so) => cmd_admin(admin_rpc_cli, rpc_host, AdminRPC::Stats(so)).await, Command::Stats(so) => cmd_admin(admin_rpc_cli, rpc_host, AdminRpc::Stats(so)).await,
_ => unreachable!(), _ => unreachable!(),
} }
} }
@@ -339,7 +343,12 @@ pub async fn cmd_status(
if let Some(cfg) = config.members.get(&adv.id) { if let Some(cfg) = config.members.get(&adv.id) {
println!( println!(
"{:?}\t{}\t{}\t[{}]\t{}\t{}", "{:?}\t{}\t{}\t[{}]\t{}\t{}",
adv.id, adv.state_info.hostname, adv.addr, cfg.tag, cfg.datacenter, cfg.capacity adv.id,
adv.state_info.hostname,
adv.addr,
cfg.tag,
cfg.zone,
cfg.capacity_string()
); );
} else { } else {
println!( println!(
@@ -365,8 +374,8 @@ pub async fn cmd_status(
adv.state_info.hostname, adv.state_info.hostname,
adv.addr, adv.addr,
cfg.tag, cfg.tag,
cfg.datacenter, cfg.zone,
cfg.capacity, cfg.capacity_string(),
(now_msec() - adv.last_seen) / 1000, (now_msec() - adv.last_seen) / 1000,
); );
} }
@@ -375,7 +384,10 @@ pub async fn cmd_status(
if !status.iter().any(|x| x.id == *id) { if !status.iter().any(|x| x.id == *id) {
println!( println!(
"{:?}\t{}\t{}\t{}\tnever seen", "{:?}\t{}\t{}\t{}\tnever seen",
id, cfg.tag, cfg.datacenter, cfg.capacity id,
cfg.tag,
cfg.zone,
cfg.capacity_string(),
); );
} }
} }
@@ -385,9 +397,9 @@ pub async fn cmd_status(
} }
pub fn find_matching_node( pub fn find_matching_node(
cand: impl std::iter::Iterator<Item = UUID>, cand: impl std::iter::Iterator<Item = Uuid>,
pattern: &str, pattern: &str,
) -> Result<UUID, Error> { ) -> Result<Uuid, Error> {
let mut candidates = vec![]; let mut candidates = vec![];
for c in cand { for c in cand {
if hex::encode(&c).starts_with(&pattern) { if hex::encode(&c).starts_with(&pattern) {
@@ -438,21 +450,40 @@ pub async fn cmd_configure(
} }
} }
if args.capacity.is_some() && args.gateway {
return Err(Error::Message(
"-c and -g are mutually exclusive, please configure node either with c>0 to act as a storage node or with -g to act as a gateway node".into()));
}
if args.capacity == Some(0) {
return Err(Error::Message("Invalid capacity value: 0".into()));
}
let new_entry = match config.members.get(&added_node) { let new_entry = match config.members.get(&added_node) {
None => NetworkConfigEntry { None => {
datacenter: args let capacity = match args.capacity {
.datacenter Some(c) => Some(c),
.expect("Please specifiy a datacenter with the -d flag"), None if args.gateway => None,
capacity: args _ => return Err(Error::Message(
.capacity "Please specify a capacity with the -c flag, or set node explicitly as gateway with -g".into())),
.expect("Please specifiy a capacity with the -c flag"), };
tag: args.tag.unwrap_or("".to_string()), NetworkConfigEntry {
}, zone: args.zone.expect("Please specifiy a zone with the -z flag"),
Some(old) => NetworkConfigEntry { capacity,
datacenter: args.datacenter.unwrap_or(old.datacenter.to_string()), tag: args.tag.unwrap_or_default(),
capacity: args.capacity.unwrap_or(old.capacity), }
tag: args.tag.unwrap_or(old.tag.to_string()), }
}, Some(old) => {
let capacity = match args.capacity {
Some(c) => Some(c),
None if args.gateway => None,
_ => old.capacity,
};
NetworkConfigEntry {
zone: args.zone.unwrap_or_else(|| old.zone.to_string()),
capacity,
tag: args.tag.unwrap_or_else(|| old.tag.to_string()),
}
}
}; };
config.members.insert(added_node, new_entry); config.members.insert(added_node, new_entry);
@@ -504,30 +535,30 @@ pub async fn cmd_remove(
} }
pub async fn cmd_admin( pub async fn cmd_admin(
rpc_cli: RpcAddrClient<AdminRPC>, rpc_cli: RpcAddrClient<AdminRpc>,
rpc_host: SocketAddr, rpc_host: SocketAddr,
args: AdminRPC, args: AdminRpc,
) -> Result<(), Error> { ) -> Result<(), Error> {
match rpc_cli.call(&rpc_host, args, ADMIN_RPC_TIMEOUT).await?? { match rpc_cli.call(&rpc_host, args, ADMIN_RPC_TIMEOUT).await?? {
AdminRPC::Ok(msg) => { AdminRpc::Ok(msg) => {
println!("{}", msg); println!("{}", msg);
} }
AdminRPC::BucketList(bl) => { AdminRpc::BucketList(bl) => {
println!("List of buckets:"); println!("List of buckets:");
for bucket in bl { for bucket in bl {
println!("{}", bucket); println!("{}", bucket);
} }
} }
AdminRPC::BucketInfo(bucket) => { AdminRpc::BucketInfo(bucket) => {
print_bucket_info(&bucket); print_bucket_info(&bucket);
} }
AdminRPC::KeyList(kl) => { AdminRpc::KeyList(kl) => {
println!("List of keys:"); println!("List of keys:");
for key in kl { for key in kl {
println!("{}\t{}", key.0, key.1); println!("{}\t{}", key.0, key.1);
} }
} }
AdminRPC::KeyInfo(key) => { AdminRpc::KeyInfo(key) => {
print_key_info(&key); print_key_info(&key);
} }
r => { r => {
+10 -5
View File
@@ -25,7 +25,7 @@ async fn shutdown_signal(send_cancel: watch::Sender<bool>) -> Result<(), Error>
Ok(()) Ok(())
} }
async fn wait_from(mut chan: watch::Receiver<bool>) -> () { async fn wait_from(mut chan: watch::Receiver<bool>) {
while !*chan.borrow() { while !*chan.borrow() {
if chan.changed().await.is_err() { if chan.changed().await.is_err() {
return; return;
@@ -40,10 +40,15 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> {
info!("Opening database..."); info!("Opening database...");
let mut db_path = config.metadata_dir.clone(); let mut db_path = config.metadata_dir.clone();
db_path.push("db"); db_path.push("db");
let db = sled::open(&db_path).expect("Unable to open sled DB"); let db = sled::Config::default()
.path(&db_path)
.cache_capacity(config.sled_cache_capacity)
.flush_every_ms(Some(config.sled_flush_every_ms))
.open()
.expect("Unable to open sled DB");
info!("Initialize RPC server..."); info!("Initialize RPC server...");
let mut rpc_server = RpcServer::new(config.rpc_bind_addr.clone(), config.rpc_tls.clone()); let mut rpc_server = RpcServer::new(config.rpc_bind_addr, config.rpc_tls.clone());
info!("Initializing background runner..."); info!("Initializing background runner...");
let (send_cancel, watch_cancel) = watch::channel(false); let (send_cancel, watch_cancel) = watch::channel(false);
@@ -66,9 +71,9 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> {
let web_server = run_web_server(garage, wait_from(watch_cancel.clone())); let web_server = run_web_server(garage, wait_from(watch_cancel.clone()));
futures::try_join!( futures::try_join!(
bootstrap.map(|rv| { bootstrap.map(|()| {
info!("Bootstrap done"); info!("Bootstrap done");
Ok(rv) Ok(())
}), }),
run_rpc_server.map(|rv| { run_rpc_server.map(|rv| {
info!("RPC server exited"); info!("RPC server exited");
+4 -4
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_model" name = "garage_model"
version = "0.2.1" version = "0.3.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
@@ -13,9 +13,9 @@ path = "lib.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
garage_rpc = { version = "0.2.1", path = "../rpc" } garage_rpc = { version = "0.3.0", path = "../rpc" }
garage_table = { version = "0.2.1", path = "../table" } garage_table = { version = "0.3.0", path = "../table" }
garage_util = { version = "0.2.1", path = "../util" } garage_util = { version = "0.3.0", path = "../util" }
arc-swap = "1.0" arc-swap = "1.0"
hex = "0.4" hex = "0.4"
+15 -15
View File
@@ -1,4 +1,4 @@
use std::path::PathBuf; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@@ -137,7 +137,7 @@ impl BlockManager {
Message::PutBlock(m) => self.write_block(&m.hash, &m.data).await, Message::PutBlock(m) => self.write_block(&m.hash, &m.data).await,
Message::GetBlock(h) => self.read_block(h).await, Message::GetBlock(h) => self.read_block(h).await,
Message::NeedBlockQuery(h) => self.need_block(h).await.map(Message::NeedBlockReply), Message::NeedBlockQuery(h) => self.need_block(h).await.map(Message::NeedBlockReply),
_ => Err(Error::BadRPC(format!("Unexpected RPC message"))), _ => Err(Error::BadRpc("Unexpected RPC message".to_string())),
} }
} }
@@ -280,8 +280,8 @@ impl BlockManager {
if let Err(e) = self.resync_iter(&mut must_exit).await { if let Err(e) = self.resync_iter(&mut must_exit).await {
warn!("Error in block resync loop: {}", e); warn!("Error in block resync loop: {}", e);
select! { select! {
_ = tokio::time::sleep(Duration::from_secs(1)).fuse() => (), _ = tokio::time::sleep(Duration::from_secs(1)).fuse() => {},
_ = must_exit.changed().fuse() => (), _ = must_exit.changed().fuse() => {},
} }
} }
} }
@@ -304,15 +304,15 @@ impl BlockManager {
} else { } else {
let delay = tokio::time::sleep(Duration::from_millis(time_msec - now)); let delay = tokio::time::sleep(Duration::from_millis(time_msec - now));
select! { select! {
_ = delay.fuse() => (), _ = delay.fuse() => {},
_ = self.resync_notify.notified().fuse() => (), _ = self.resync_notify.notified().fuse() => {},
_ = must_exit.changed().fuse() => (), _ = must_exit.changed().fuse() => {},
} }
} }
} else { } else {
select! { select! {
_ = self.resync_notify.notified().fuse() => (), _ = self.resync_notify.notified().fuse() => {},
_ = must_exit.changed().fuse() => (), _ = must_exit.changed().fuse() => {},
} }
} }
Ok(()) Ok(())
@@ -342,7 +342,7 @@ impl BlockManager {
let mut who = self.replication.write_nodes(&hash); let mut who = self.replication.write_nodes(&hash);
if who.len() < self.replication.write_quorum() { if who.len() < self.replication.write_quorum() {
return Err(Error::Message(format!("Not trying to offload block because we don't have a quorum of nodes to write to"))); return Err(Error::Message("Not trying to offload block because we don't have a quorum of nodes to write to".to_string()));
} }
who.retain(|id| *id != self.system.id); who.retain(|id| *id != self.system.id);
@@ -362,14 +362,14 @@ impl BlockManager {
} }
} }
_ => { _ => {
return Err(Error::Message(format!( return Err(Error::Message(
"Unexpected response to NeedBlockQuery RPC" "Unexpected response to NeedBlockQuery RPC".to_string(),
))); ));
} }
} }
} }
if need_nodes.len() > 0 { if !need_nodes.is_empty() {
trace!( trace!(
"Block {:?} needed by {} nodes, sending", "Block {:?} needed by {} nodes, sending",
hash, hash,
@@ -478,7 +478,7 @@ impl BlockManager {
fn repair_aux_read_dir_rec<'a>( fn repair_aux_read_dir_rec<'a>(
&'a self, &'a self,
path: &'a PathBuf, path: &'a Path,
must_exit: &'a watch::Receiver<bool>, must_exit: &'a watch::Receiver<bool>,
) -> BoxFuture<'a, Result<(), Error>> { ) -> BoxFuture<'a, Result<(), Error>> {
// Lists all blocks on disk and adds them to the resync queue. // Lists all blocks on disk and adds them to the resync queue.
+7 -7
View File
@@ -3,7 +3,7 @@ use std::sync::Arc;
use garage_util::data::*; use garage_util::data::*;
use garage_table::crdt::CRDT; use garage_table::crdt::Crdt;
use garage_table::*; use garage_table::*;
use crate::block::*; use crate::block::*;
@@ -14,18 +14,18 @@ pub struct BlockRef {
pub block: Hash, pub block: Hash,
/// Id of the Version for the object containing this block, used as sorting key /// Id of the Version for the object containing this block, used as sorting key
pub version: UUID, pub version: Uuid,
// Keep track of deleted status // Keep track of deleted status
/// Is the Version that contains this block deleted /// Is the Version that contains this block deleted
pub deleted: crdt::Bool, pub deleted: crdt::Bool,
} }
impl Entry<Hash, UUID> for BlockRef { impl Entry<Hash, Uuid> for BlockRef {
fn partition_key(&self) -> &Hash { fn partition_key(&self) -> &Hash {
&self.block &self.block
} }
fn sort_key(&self) -> &UUID { fn sort_key(&self) -> &Uuid {
&self.version &self.version
} }
fn is_tombstone(&self) -> bool { fn is_tombstone(&self) -> bool {
@@ -33,7 +33,7 @@ impl Entry<Hash, UUID> for BlockRef {
} }
} }
impl CRDT for BlockRef { impl Crdt for BlockRef {
fn merge(&mut self, other: &Self) { fn merge(&mut self, other: &Self) {
self.deleted.merge(&other.deleted); self.deleted.merge(&other.deleted);
} }
@@ -45,12 +45,12 @@ pub struct BlockRefTable {
impl TableSchema for BlockRefTable { impl TableSchema for BlockRefTable {
type P = Hash; type P = Hash;
type S = UUID; type S = Uuid;
type E = BlockRef; type E = BlockRef;
type Filter = DeletedFilter; type Filter = DeletedFilter;
fn updated(&self, old: Option<Self::E>, new: Option<Self::E>) { fn updated(&self, old: Option<Self::E>, new: Option<Self::E>) {
let block = &old.as_ref().or(new.as_ref()).unwrap().block; let block = &old.as_ref().or_else(|| new.as_ref()).unwrap().block;
let was_before = old.as_ref().map(|x| !x.deleted.get()).unwrap_or(false); let was_before = old.as_ref().map(|x| !x.deleted.get()).unwrap_or(false);
let is_after = new.as_ref().map(|x| !x.deleted.get()).unwrap_or(false); let is_after = new.as_ref().map(|x| !x.deleted.get()).unwrap_or(false);
if is_after && !was_before { if is_after && !was_before {
+22 -16
View File
@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use garage_table::crdt::CRDT; use garage_table::crdt::Crdt;
use garage_table::*; use garage_table::*;
use crate::key_table::PermissionSet; use crate::key_table::PermissionSet;
@@ -15,7 +15,7 @@ pub struct Bucket {
/// Name of the bucket /// Name of the bucket
pub name: String, pub name: String,
/// State, and configuration if not deleted, of the bucket /// State, and configuration if not deleted, of the bucket
pub state: crdt::LWW<BucketState>, pub state: crdt::Lww<BucketState>,
} }
/// State of a bucket /// State of a bucket
@@ -27,7 +27,7 @@ pub enum BucketState {
Present(BucketParams), Present(BucketParams),
} }
impl CRDT for BucketState { impl Crdt for BucketState {
fn merge(&mut self, o: &Self) { fn merge(&mut self, o: &Self) {
match o { match o {
BucketState::Deleted => *self = BucketState::Deleted, BucketState::Deleted => *self = BucketState::Deleted,
@@ -44,34 +44,40 @@ impl CRDT for BucketState {
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub struct BucketParams { pub struct BucketParams {
/// Map of key with access to the bucket, and what kind of access they give /// Map of key with access to the bucket, and what kind of access they give
pub authorized_keys: crdt::LWWMap<String, PermissionSet>, pub authorized_keys: crdt::LwwMap<String, PermissionSet>,
/// Is the bucket served as http /// Is the bucket served as http
pub website: crdt::LWW<bool>, 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 BucketParams { impl BucketParams {
/// Create an empty BucketParams with no authorized keys and no website accesss /// Create an empty BucketParams with no authorized keys and no website accesss
pub fn new() -> Self { pub fn new() -> Self {
BucketParams { BucketParams {
authorized_keys: crdt::LWWMap::new(), authorized_keys: crdt::LwwMap::new(),
website: crdt::LWW::new(false), website: crdt::Lww::new(false),
} }
} }
} }
impl Crdt for BucketParams {
fn merge(&mut self, o: &Self) {
self.authorized_keys.merge(&o.authorized_keys);
self.website.merge(&o.website);
}
}
impl Default for BucketParams {
fn default() -> Self {
Self::new()
}
}
impl Bucket { impl Bucket {
/// Initializes a new instance of the Bucket struct /// Initializes a new instance of the Bucket struct
pub fn new(name: String) -> Self { pub fn new(name: String) -> Self {
Bucket { Bucket {
name, name,
state: crdt::LWW::new(BucketState::Present(BucketParams::new())), state: crdt::Lww::new(BucketState::Present(BucketParams::new())),
} }
} }
@@ -99,7 +105,7 @@ impl Entry<EmptyKey, String> for Bucket {
} }
} }
impl CRDT for Bucket { impl Crdt for Bucket {
fn merge(&mut self, other: &Self) { fn merge(&mut self, other: &Self) {
self.state.merge(&other.state); self.state.merge(&other.state);
} }
+17 -12
View File
@@ -7,6 +7,7 @@ use garage_rpc::membership::System;
use garage_rpc::rpc_client::RpcHttpClient; use garage_rpc::rpc_client::RpcHttpClient;
use garage_rpc::rpc_server::RpcServer; use garage_rpc::rpc_server::RpcServer;
use garage_table::replication::ReplicationMode;
use garage_table::replication::TableFullReplication; use garage_table::replication::TableFullReplication;
use garage_table::replication::TableShardedReplication; use garage_table::replication::TableShardedReplication;
use garage_table::*; use garage_table::*;
@@ -50,6 +51,9 @@ impl Garage {
background: Arc<BackgroundRunner>, background: Arc<BackgroundRunner>,
rpc_server: &mut RpcServer, rpc_server: &mut RpcServer,
) -> Arc<Self> { ) -> Arc<Self> {
let replication_mode = ReplicationMode::parse(&config.replication_mode)
.expect("Invalid replication_mode in config file.");
info!("Initialize membership management system..."); info!("Initialize membership management system...");
let rpc_http_client = Arc::new( let rpc_http_client = Arc::new(
RpcHttpClient::new(config.max_concurrent_rpc_requests, &config.rpc_tls) RpcHttpClient::new(config.max_concurrent_rpc_requests, &config.rpc_tls)
@@ -60,32 +64,33 @@ impl Garage {
rpc_http_client, rpc_http_client,
background.clone(), background.clone(),
rpc_server, rpc_server,
replication_mode.replication_factor(),
); );
let data_rep_param = TableShardedReplication { let data_rep_param = TableShardedReplication {
system: system.clone(), system: system.clone(),
replication_factor: config.data_replication_factor, replication_factor: replication_mode.replication_factor(),
write_quorum: (config.data_replication_factor + 1) / 2, write_quorum: replication_mode.write_quorum(),
read_quorum: 1, read_quorum: 1,
}; };
let meta_rep_param = TableShardedReplication { let meta_rep_param = TableShardedReplication {
system: system.clone(), system: system.clone(),
replication_factor: config.meta_replication_factor, replication_factor: replication_mode.replication_factor(),
write_quorum: (config.meta_replication_factor + 1) / 2, write_quorum: replication_mode.write_quorum(),
read_quorum: (config.meta_replication_factor + 1) / 2, read_quorum: replication_mode.read_quorum(),
}; };
let control_rep_param = TableFullReplication { let control_rep_param = TableFullReplication {
system: system.clone(), system: system.clone(),
max_faults: config.control_write_max_faults, max_faults: replication_mode.control_write_max_faults(),
}; };
info!("Initialize block manager..."); info!("Initialize block manager...");
let block_manager = BlockManager::new( let block_manager = BlockManager::new(
&db, &db,
config.data_dir.clone(), config.data_dir.clone(),
data_rep_param.clone(), data_rep_param,
system.clone(), system.clone(),
rpc_server, rpc_server,
); );
@@ -95,7 +100,7 @@ impl Garage {
BlockRefTable { BlockRefTable {
block_manager: block_manager.clone(), block_manager: block_manager.clone(),
}, },
data_rep_param.clone(), meta_rep_param.clone(),
system.clone(), system.clone(),
&db, &db,
"block_ref".to_string(), "block_ref".to_string(),
@@ -121,7 +126,7 @@ impl Garage {
background: background.clone(), background: background.clone(),
version_table: version_table.clone(), version_table: version_table.clone(),
}, },
meta_rep_param.clone(), meta_rep_param,
system.clone(), system.clone(),
&db, &db,
"object".to_string(), "object".to_string(),
@@ -141,7 +146,7 @@ impl Garage {
info!("Initialize key_table_table..."); info!("Initialize key_table_table...");
let key_table = Table::new( let key_table = Table::new(
KeyTable, KeyTable,
control_rep_param.clone(), control_rep_param,
system.clone(), system.clone(),
&db, &db,
"key".to_string(), "key".to_string(),
@@ -152,9 +157,9 @@ impl Garage {
let garage = Arc::new(Self { let garage = Arc::new(Self {
config, config,
db, db,
system: system.clone(),
block_manager,
background, background,
system,
block_manager,
bucket_table, bucket_table,
key_table, key_table,
object_table, object_table,
+10 -10
View File
@@ -13,14 +13,14 @@ pub struct Key {
pub secret_key: String, pub secret_key: String,
/// Name for the key /// Name for the key
pub name: crdt::LWW<String>, pub name: crdt::Lww<String>,
/// Is the key deleted /// Is the key deleted
pub deleted: crdt::Bool, pub deleted: crdt::Bool,
/// Buckets in which the key is authorized. Empty if `Key` is deleted /// Buckets in which the key is authorized. Empty if `Key` is deleted
// CRDT interaction: deleted implies authorized_buckets is empty // CRDT interaction: deleted implies authorized_buckets is empty
pub authorized_buckets: crdt::LWWMap<String, PermissionSet>, pub authorized_buckets: crdt::LwwMap<String, PermissionSet>,
} }
impl Key { impl Key {
@@ -31,9 +31,9 @@ impl Key {
Self { Self {
key_id, key_id,
secret_key, secret_key,
name: crdt::LWW::new(name), name: crdt::Lww::new(name),
deleted: crdt::Bool::new(false), deleted: crdt::Bool::new(false),
authorized_buckets: crdt::LWWMap::new(), authorized_buckets: crdt::LwwMap::new(),
} }
} }
@@ -42,9 +42,9 @@ impl Key {
Self { Self {
key_id: key_id.to_string(), key_id: key_id.to_string(),
secret_key: secret_key.to_string(), secret_key: secret_key.to_string(),
name: crdt::LWW::new(name.to_string()), name: crdt::Lww::new(name.to_string()),
deleted: crdt::Bool::new(false), deleted: crdt::Bool::new(false),
authorized_buckets: crdt::LWWMap::new(), authorized_buckets: crdt::LwwMap::new(),
} }
} }
@@ -53,9 +53,9 @@ impl Key {
Self { Self {
key_id, key_id,
secret_key: "".into(), secret_key: "".into(),
name: crdt::LWW::new("".to_string()), name: crdt::Lww::new("".to_string()),
deleted: crdt::Bool::new(true), deleted: crdt::Bool::new(true),
authorized_buckets: crdt::LWWMap::new(), authorized_buckets: crdt::LwwMap::new(),
} }
} }
@@ -85,7 +85,7 @@ pub struct PermissionSet {
pub allow_write: bool, pub allow_write: bool,
} }
impl AutoCRDT for PermissionSet { impl AutoCrdt for PermissionSet {
const WARN_IF_DIFFERENT: bool = true; const WARN_IF_DIFFERENT: bool = true;
} }
@@ -98,7 +98,7 @@ impl Entry<EmptyKey, String> for Key {
} }
} }
impl CRDT for Key { impl Crdt for Key {
fn merge(&mut self, other: &Self) { fn merge(&mut self, other: &Self) {
self.name.merge(&other.name); self.name.merge(&other.name);
self.deleted.merge(&other.deleted); self.deleted.merge(&other.deleted);
+9 -15
View File
@@ -40,6 +40,7 @@ impl Object {
} }
/// Adds a version if it wasn't already present /// Adds a version if it wasn't already present
#[allow(clippy::result_unit_err)]
pub fn add_version(&mut self, new: ObjectVersion) -> Result<(), ()> { pub fn add_version(&mut self, new: ObjectVersion) -> Result<(), ()> {
match self match self
.versions .versions
@@ -63,7 +64,7 @@ impl Object {
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub struct ObjectVersion { pub struct ObjectVersion {
/// Id of the version /// Id of the version
pub uuid: UUID, pub uuid: Uuid,
/// Timestamp of when the object was created /// Timestamp of when the object was created
pub timestamp: u64, pub timestamp: u64,
/// State of the version /// State of the version
@@ -81,7 +82,7 @@ pub enum ObjectVersionState {
Aborted, Aborted,
} }
impl CRDT for ObjectVersionState { impl Crdt for ObjectVersionState {
fn merge(&mut self, other: &Self) { fn merge(&mut self, other: &Self) {
use ObjectVersionState::*; use ObjectVersionState::*;
match other { match other {
@@ -114,7 +115,7 @@ pub enum ObjectVersionData {
FirstBlock(ObjectVersionMeta, Hash), FirstBlock(ObjectVersionMeta, Hash),
} }
impl AutoCRDT for ObjectVersionData { impl AutoCrdt for ObjectVersionData {
const WARN_IF_DIFFERENT: bool = true; const WARN_IF_DIFFERENT: bool = true;
} }
@@ -139,24 +140,18 @@ pub struct ObjectVersionHeaders {
} }
impl ObjectVersion { impl ObjectVersion {
fn cmp_key(&self) -> (u64, UUID) { fn cmp_key(&self) -> (u64, Uuid) {
(self.timestamp, self.uuid) (self.timestamp, self.uuid)
} }
/// Is the object version currently being uploaded /// Is the object version currently being uploaded
pub fn is_uploading(&self) -> bool { pub fn is_uploading(&self) -> bool {
match self.state { matches!(self.state, ObjectVersionState::Uploading(_))
ObjectVersionState::Uploading(_) => true,
_ => false,
}
} }
/// Is the object version completely received /// Is the object version completely received
pub fn is_complete(&self) -> bool { pub fn is_complete(&self) -> bool {
match self.state { matches!(self.state, ObjectVersionState::Complete(_))
ObjectVersionState::Complete(_) => true,
_ => false,
}
} }
/// Is the object version available (received and not a tombstone) /// Is the object version available (received and not a tombstone)
@@ -183,7 +178,7 @@ impl Entry<String, String> for Object {
} }
} }
impl CRDT for Object { impl Crdt for Object {
fn merge(&mut self, other: &Self) { fn merge(&mut self, other: &Self) {
// Merge versions from other into here // Merge versions from other into here
for other_v in other.versions.iter() { for other_v in other.versions.iter() {
@@ -207,8 +202,7 @@ impl CRDT for Object {
.iter() .iter()
.enumerate() .enumerate()
.rev() .rev()
.filter(|(_, v)| v.is_complete()) .find(|(_, v)| v.is_complete())
.next()
.map(|(vi, _)| vi); .map(|(vi, _)| vi);
if let Some(last_vi) = last_complete { if let Some(last_vi) = last_complete {
+4 -4
View File
@@ -14,7 +14,7 @@ use crate::block_ref_table::*;
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub struct Version { pub struct Version {
/// UUID of the version, used as partition key /// UUID of the version, used as partition key
pub uuid: UUID, pub uuid: Uuid,
// Actual data: the blocks for this version // Actual data: the blocks for this version
// In the case of a multipart upload, also store the etags // In the case of a multipart upload, also store the etags
@@ -35,7 +35,7 @@ pub struct Version {
} }
impl Version { impl Version {
pub fn new(uuid: UUID, bucket: String, key: String, deleted: bool) -> Self { pub fn new(uuid: Uuid, bucket: String, key: String, deleted: bool) -> Self {
Self { Self {
uuid, uuid,
deleted: deleted.into(), deleted: deleted.into(),
@@ -78,7 +78,7 @@ pub struct VersionBlock {
pub size: u64, pub size: u64,
} }
impl AutoCRDT for VersionBlock { impl AutoCrdt for VersionBlock {
const WARN_IF_DIFFERENT: bool = true; const WARN_IF_DIFFERENT: bool = true;
} }
@@ -94,7 +94,7 @@ impl Entry<Hash, EmptyKey> for Version {
} }
} }
impl CRDT for Version { impl Crdt for Version {
fn merge(&mut self, other: &Self) { fn merge(&mut self, other: &Self) {
self.deleted.merge(&other.deleted); self.deleted.merge(&other.deleted);
+4 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_rpc" name = "garage_rpc"
version = "0.2.1" version = "0.3.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
@@ -13,7 +13,9 @@ path = "lib.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
garage_util = { version = "0.2.1", path = "../util" } garage_util = { version = "0.3.0", path = "../util" }
garage_rpc_021 = { package = "garage_rpc", version = "0.2.1" }
arc-swap = "1.0" arc-swap = "1.0"
bytes = "1.0" bytes = "1.0"
+57 -51
View File
@@ -3,7 +3,7 @@ use std::collections::HashMap;
use std::fmt::Write as FmtWrite; use std::fmt::Write as FmtWrite;
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::net::{IpAddr, SocketAddr}; use std::net::{IpAddr, SocketAddr};
use std::path::PathBuf; use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@@ -56,7 +56,7 @@ impl RpcMessage for Message {}
/// A ping, containing informations about status and config /// A ping, containing informations about status and config
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct PingMessage { pub struct PingMessage {
id: UUID, id: Uuid,
rpc_port: u16, rpc_port: u16,
status_hash: Hash, status_hash: Hash,
@@ -69,7 +69,7 @@ pub struct PingMessage {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AdvertisedNode { pub struct AdvertisedNode {
/// Id of the node this advertisement relates to /// Id of the node this advertisement relates to
pub id: UUID, pub id: Uuid,
/// IP and port of the node /// IP and port of the node
pub addr: SocketAddr, pub addr: SocketAddr,
@@ -84,7 +84,7 @@ pub struct AdvertisedNode {
/// This node's membership manager /// This node's membership manager
pub struct System { pub struct System {
/// The id of this node /// The id of this node
pub id: UUID, pub id: Uuid,
persist_config: Persister<NetworkConfig>, persist_config: Persister<NetworkConfig>,
persist_status: Persister<Vec<AdvertisedNode>>, persist_status: Persister<Vec<AdvertisedNode>>,
@@ -95,6 +95,7 @@ pub struct System {
rpc_http_client: Arc<RpcHttpClient>, rpc_http_client: Arc<RpcHttpClient>,
rpc_client: Arc<RpcClient<Message>>, rpc_client: Arc<RpcClient<Message>>,
replication_factor: usize,
pub(crate) status: watch::Receiver<Arc<Status>>, pub(crate) status: watch::Receiver<Arc<Status>>,
/// The ring /// The ring
pub ring: watch::Receiver<Arc<Ring>>, pub ring: watch::Receiver<Arc<Ring>>,
@@ -114,7 +115,7 @@ struct Updaters {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Status { pub struct Status {
/// Mapping of each node id to its known status /// Mapping of each node id to its known status
pub nodes: HashMap<UUID, Arc<StatusEntry>>, pub nodes: HashMap<Uuid, Arc<StatusEntry>>,
/// Hash of `nodes`, used to detect when nodes have different views of the cluster /// Hash of `nodes`, used to detect when nodes have different views of the cluster
pub hash: Hash, pub hash: Hash,
} }
@@ -198,15 +199,15 @@ impl Status {
} }
} }
fn gen_node_id(metadata_dir: &PathBuf) -> Result<UUID, Error> { fn gen_node_id(metadata_dir: &Path) -> Result<Uuid, Error> {
let mut id_file = metadata_dir.clone(); let mut id_file = metadata_dir.to_path_buf();
id_file.push("node_id"); id_file.push("node_id");
if id_file.as_path().exists() { if id_file.as_path().exists() {
let mut f = std::fs::File::open(id_file.as_path())?; let mut f = std::fs::File::open(id_file.as_path())?;
let mut d = vec![]; let mut d = vec![];
f.read_to_end(&mut d)?; f.read_to_end(&mut d)?;
if d.len() != 32 { if d.len() != 32 {
return Err(Error::Message(format!("Corrupt node_id file"))); return Err(Error::Message("Corrupt node_id file".to_string()));
} }
let mut id = [0u8; 32]; let mut id = [0u8; 32];
@@ -228,6 +229,7 @@ impl System {
rpc_http_client: Arc<RpcHttpClient>, rpc_http_client: Arc<RpcHttpClient>,
background: Arc<BackgroundRunner>, background: Arc<BackgroundRunner>,
rpc_server: &mut RpcServer, rpc_server: &mut RpcServer,
replication_factor: usize,
) -> Arc<Self> { ) -> Arc<Self> {
let id = gen_node_id(&metadata_dir).expect("Unable to read or generate node ID"); let id = gen_node_id(&metadata_dir).expect("Unable to read or generate node ID");
info!("Node ID: {}", hex::encode(&id)); info!("Node ID: {}", hex::encode(&id));
@@ -238,11 +240,21 @@ impl System {
let net_config = match persist_config.load() { let net_config = match persist_config.load() {
Ok(x) => x, Ok(x) => x,
Err(e) => { Err(e) => {
info!( match Persister::<garage_rpc_021::ring::NetworkConfig>::new(
"No valid previous network configuration stored ({}), starting fresh.", &metadata_dir,
e "network_config",
); )
NetworkConfig::new() .load()
{
Ok(old_config) => NetworkConfig::migrate_from_021(old_config),
Err(e2) => {
info!(
"No valid previous network configuration stored ({}, {}), starting fresh.",
e, e2
);
NetworkConfig::new()
}
}
} }
}; };
@@ -256,10 +268,10 @@ impl System {
let state_info = StateInfo { let state_info = StateInfo {
hostname: gethostname::gethostname() hostname: gethostname::gethostname()
.into_string() .into_string()
.unwrap_or("<invalid utf-8>".to_string()), .unwrap_or_else(|_| "<invalid utf-8>".to_string()),
}; };
let ring = Ring::new(net_config); let ring = Ring::new(net_config, replication_factor);
let (update_ring, ring) = watch::channel(Arc::new(ring)); let (update_ring, ring) = watch::channel(Arc::new(ring));
let rpc_path = MEMBERSHIP_RPC_PATH.to_string(); let rpc_path = MEMBERSHIP_RPC_PATH.to_string();
@@ -277,6 +289,7 @@ impl System {
state_info, state_info,
rpc_http_client, rpc_http_client,
rpc_client, rpc_client,
replication_factor,
status, status,
ring, ring,
update_lock: Mutex::new(Updaters { update_lock: Mutex::new(Updaters {
@@ -296,12 +309,12 @@ impl System {
match msg { match msg {
Message::Ping(ping) => self2.handle_ping(&addr, &ping).await, Message::Ping(ping) => self2.handle_ping(&addr, &ping).await,
Message::PullStatus => self2.handle_pull_status(), Message::PullStatus => Ok(self2.handle_pull_status()),
Message::PullConfig => self2.handle_pull_config(), Message::PullConfig => Ok(self2.handle_pull_config()),
Message::AdvertiseNodesUp(adv) => self2.handle_advertise_nodes_up(&adv).await, Message::AdvertiseNodesUp(adv) => self2.handle_advertise_nodes_up(&adv).await,
Message::AdvertiseConfig(adv) => self2.handle_advertise_config(&adv).await, Message::AdvertiseConfig(adv) => self2.handle_advertise_config(&adv).await,
_ => Err(Error::BadRPC(format!("Unexpected RPC message"))), _ => Err(Error::BadRpc("Unexpected RPC message".to_string())),
} }
} }
}); });
@@ -358,18 +371,18 @@ impl System {
) { ) {
let self2 = self.clone(); let self2 = self.clone();
self.background self.background
.spawn_worker(format!("discovery loop"), |stop_signal| { .spawn_worker("discovery loop".to_string(), |stop_signal| {
self2.discovery_loop(peers, consul_host, consul_service_name, stop_signal) self2.discovery_loop(peers, consul_host, consul_service_name, stop_signal)
}); });
let self2 = self.clone(); let self2 = self.clone();
self.background self.background
.spawn_worker(format!("ping loop"), |stop_signal| { .spawn_worker("ping loop".to_string(), |stop_signal| {
self2.ping_loop(stop_signal) self2.ping_loop(stop_signal)
}); });
} }
async fn ping_nodes(self: Arc<Self>, peers: Vec<(SocketAddr, Option<UUID>)>) { async fn ping_nodes(self: Arc<Self>, peers: Vec<(SocketAddr, Option<Uuid>)>) {
let ping_msg = self.make_ping(); let ping_msg = self.make_ping();
let ping_resps = join_all(peers.iter().map(|(addr, id_option)| { let ping_resps = join_all(peers.iter().map(|(addr, id_option)| {
let sys = self.clone(); let sys = self.clone();
@@ -424,7 +437,6 @@ impl System {
warn!("Node {:?} seems to be down.", id); warn!("Node {:?} seems to be down.", id);
if !ring.config.members.contains_key(id) { if !ring.config.members.contains_key(id) {
info!("Removing node {:?} from status (not in config and not responding to pings anymore)", id); info!("Removing node {:?} from status (not in config and not responding to pings anymore)", id);
drop(st);
status.nodes.remove(&id); status.nodes.remove(&id);
has_changes = true; has_changes = true;
} }
@@ -438,7 +450,7 @@ impl System {
self.update_status(&update_locked, status).await; self.update_status(&update_locked, status).await;
drop(update_locked); drop(update_locked);
if to_advertise.len() > 0 { if !to_advertise.is_empty() {
self.broadcast(Message::AdvertiseNodesUp(to_advertise), PING_TIMEOUT) self.broadcast(Message::AdvertiseNodesUp(to_advertise), PING_TIMEOUT)
.await; .await;
} }
@@ -474,15 +486,13 @@ impl System {
Ok(self.make_ping()) Ok(self.make_ping())
} }
fn handle_pull_status(&self) -> Result<Message, Error> { fn handle_pull_status(&self) -> Message {
Ok(Message::AdvertiseNodesUp( Message::AdvertiseNodesUp(self.status.borrow().to_serializable_membership(self))
self.status.borrow().to_serializable_membership(self),
))
} }
fn handle_pull_config(&self) -> Result<Message, Error> { fn handle_pull_config(&self) -> Message {
let ring = self.ring.borrow().clone(); let ring = self.ring.borrow().clone();
Ok(Message::AdvertiseConfig(ring.config.clone())) Message::AdvertiseConfig(ring.config.clone())
} }
async fn handle_advertise_nodes_up( async fn handle_advertise_nodes_up(
@@ -530,7 +540,7 @@ impl System {
self.update_status(&update_lock, status).await; self.update_status(&update_lock, status).await;
drop(update_lock); drop(update_lock);
if to_ping.len() > 0 { if !to_ping.is_empty() {
self.background self.background
.spawn_cancellable(self.clone().ping_nodes(to_ping).map(Ok)); .spawn_cancellable(self.clone().ping_nodes(to_ping).map(Ok));
} }
@@ -546,7 +556,7 @@ impl System {
let ring: Arc<Ring> = self.ring.borrow().clone(); let ring: Arc<Ring> = self.ring.borrow().clone();
if adv.version > ring.config.version { if adv.version > ring.config.version {
let ring = Ring::new(adv.clone()); let ring = Ring::new(adv.clone(), self.replication_factor);
update_lock.update_ring.send(Arc::new(ring))?; update_lock.update_ring.send(Arc::new(ring))?;
drop(update_lock); drop(update_lock);
@@ -576,8 +586,8 @@ impl System {
self.clone().ping_nodes(ping_addrs).await; self.clone().ping_nodes(ping_addrs).await;
select! { select! {
_ = restart_at.fuse() => (), _ = restart_at.fuse() => {},
_ = stop_signal.changed().fuse() => (), _ = stop_signal.changed().fuse() => {},
} }
} }
} }
@@ -595,7 +605,7 @@ impl System {
}; };
while !*stop_signal.borrow() { while !*stop_signal.borrow() {
let not_configured = self.ring.borrow().config.members.len() == 0; let not_configured = self.ring.borrow().config.members.is_empty();
let no_peers = self.status.borrow().nodes.len() < 3; let no_peers = self.status.borrow().nodes.len() < 3;
let bad_peers = self let bad_peers = self
.status .status
@@ -613,11 +623,8 @@ impl System {
.map(|ip| (*ip, None)) .map(|ip| (*ip, None))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
match self.persist_status.load_async().await { if let Ok(peers) = self.persist_status.load_async().await {
Ok(peers) => { ping_list.extend(peers.iter().map(|x| (x.addr, Some(x.id))));
ping_list.extend(peers.iter().map(|x| (x.addr, Some(x.id))));
}
_ => (),
} }
if let Some((consul_host, consul_service_name)) = &consul_config { if let Some((consul_host, consul_service_name)) = &consul_config {
@@ -636,15 +643,17 @@ impl System {
let restart_at = tokio::time::sleep(DISCOVERY_INTERVAL); let restart_at = tokio::time::sleep(DISCOVERY_INTERVAL);
select! { select! {
_ = restart_at.fuse() => (), _ = restart_at.fuse() => {},
_ = stop_signal.changed().fuse() => (), _ = stop_signal.changed().fuse() => {},
} }
} }
} }
// for some reason fixing this is causing compilation error, see https://github.com/rust-lang/rust-clippy/issues/7052
#[allow(clippy::manual_async_fn)]
fn pull_status( fn pull_status(
self: Arc<Self>, self: Arc<Self>,
peer: UUID, peer: Uuid,
) -> impl futures::future::Future<Output = ()> + Send + 'static { ) -> impl futures::future::Future<Output = ()> + Send + 'static {
async move { async move {
let resp = self let resp = self
@@ -657,7 +666,7 @@ impl System {
} }
} }
async fn pull_config(self: Arc<Self>, peer: UUID) { async fn pull_config(self: Arc<Self>, peer: Uuid) {
let resp = self let resp = self
.rpc_client .rpc_client
.call(peer, Message::PullConfig, PING_TIMEOUT) .call(peer, Message::PullConfig, PING_TIMEOUT)
@@ -672,18 +681,15 @@ impl System {
let mut list = status.to_serializable_membership(&self); let mut list = status.to_serializable_membership(&self);
// Combine with old peer list to make sure no peer is lost // Combine with old peer list to make sure no peer is lost
match self.persist_status.load_async().await { if let Ok(old_list) = self.persist_status.load_async().await {
Ok(old_list) => { for pp in old_list {
for pp in old_list { if !list.iter().any(|np| pp.id == np.id) {
if !list.iter().any(|np| pp.id == np.id) { list.push(pp);
list.push(pp);
}
} }
} }
_ => (),
} }
if list.len() > 0 { if !list.is_empty() {
info!("Persisting new peer list ({} peers)", list.len()); info!("Persisting new peer list ({} peers)", list.len());
self.persist_status self.persist_status
.save_async(&list) .save_async(&list)
+146 -58
View File
@@ -7,10 +7,9 @@ use serde::{Deserialize, Serialize};
use garage_util::data::*; use garage_util::data::*;
// A partition number is encoded on 16 bits, /// A partition id, which is stored on 16 bits
// i.e. we have up to 2**16 partitions. /// i.e. we have up to 2**16 partitions.
// (in practice we have exactly 2**PARTITION_BITS partitions) /// (in practice we have exactly 2**PARTITION_BITS partitions)
/// A partition id, stored on 16 bits
pub type Partition = u16; pub type Partition = u16;
// TODO: make this constant parametrizable in the config file // TODO: make this constant parametrizable in the config file
@@ -23,16 +22,11 @@ pub const PARTITION_BITS: usize = 8;
const PARTITION_MASK_U16: u16 = ((1 << PARTITION_BITS) - 1) << (16 - PARTITION_BITS); const PARTITION_MASK_U16: u16 = ((1 << PARTITION_BITS) - 1) << (16 - PARTITION_BITS);
// TODO: make this constant paraetrizable in the config file
// (most deployments use a replication factor of 3, so...)
/// The maximum number of time an object might get replicated
pub const MAX_REPLICATION: usize = 3;
/// The user-defined configuration of the cluster's nodes /// The user-defined configuration of the cluster's nodes
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NetworkConfig { pub struct NetworkConfig {
/// Map of each node's id to it's configuration /// Map of each node's id to it's configuration
pub members: HashMap<UUID, NetworkConfigEntry>, pub members: HashMap<Uuid, NetworkConfigEntry>,
/// Version of this config /// Version of this config
pub version: u64, pub version: u64,
} }
@@ -44,6 +38,31 @@ impl NetworkConfig {
version: 0, version: 0,
} }
} }
pub(crate) fn migrate_from_021(old: garage_rpc_021::ring::NetworkConfig) -> Self {
let members = old
.members
.into_iter()
.map(|(id, conf)| {
(
Hash::try_from(id.as_slice()).unwrap(),
NetworkConfigEntry {
zone: conf.datacenter,
capacity: if conf.capacity == 0 {
None
} else {
Some(conf.capacity)
},
tag: conf.tag,
},
)
})
.collect();
Self {
members,
version: old.version,
}
}
} }
/// The overall configuration of one (possibly remote) node /// The overall configuration of one (possibly remote) node
@@ -51,48 +70,80 @@ impl NetworkConfig {
pub struct NetworkConfigEntry { pub struct NetworkConfigEntry {
/// Datacenter at which this entry belong. This infromation might be used to perform a better /// Datacenter at which this entry belong. This infromation might be used to perform a better
/// geodistribution /// geodistribution
pub datacenter: String, pub zone: String,
/// The (relative) capacity of the node /// The (relative) capacity of the node
pub capacity: u32, /// If this is set to None, the node does not participate in storing data for the system
/// and is only active as an API gateway to other nodes
pub capacity: Option<u32>,
/// A tag to recognize the entry, not used for other things than display /// A tag to recognize the entry, not used for other things than display
pub tag: String, pub tag: String,
} }
impl NetworkConfigEntry {
pub fn capacity_string(&self) -> String {
match self.capacity {
Some(c) => format!("{}", c),
None => "gateway".to_string(),
}
}
}
/// A ring distributing fairly objects to nodes /// A ring distributing fairly objects to nodes
#[derive(Clone)] #[derive(Clone)]
pub struct Ring { pub struct Ring {
/// The replication factor for this ring
pub replication_factor: usize,
/// The network configuration used to generate this ring /// The network configuration used to generate this ring
pub config: NetworkConfig, pub config: NetworkConfig,
/// The list of entries in the ring
pub ring: Vec<RingEntry>, // Internal order of nodes used to make a more compact representation of the ring
nodes: Vec<Uuid>,
// The list of entries in the ring
ring: Vec<RingEntry>,
} }
// Type to store compactly the id of a node in the system
// Change this to u16 the day we want to have more than 256 nodes in a cluster
type CompactNodeType = u8;
// The maximum number of times an object might get replicated
// This must be at least 3 because Garage supports 3-way replication
// Here we use 6 so that the size of a ring entry is 8 bytes
// (2 bytes partition id, 6 bytes node numbers as u8s)
const MAX_REPLICATION: usize = 6;
/// An entry in the ring /// An entry in the ring
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct RingEntry { struct RingEntry {
/// The prefix of the Hash of object which should use this entry // The two first bytes of the first hash that goes in this partition
pub location: Hash, // (the next bytes are zeroes)
/// The nodes in which a matching object should get stored hash_prefix: u16,
pub nodes: [UUID; MAX_REPLICATION], // The nodes that store this partition, stored as a list of positions in the `nodes`
// field of the Ring structure
// Only items 0 up to ring.replication_factor - 1 are used, others are zeros
nodes_buf: [CompactNodeType; MAX_REPLICATION],
} }
impl Ring { impl Ring {
// TODO this function MUST be refactored, it's 100 lines long, with a 50 lines loop, going up to 6 // TODO this function MUST be refactored, it's 100 lines long, with a 50 lines loop, going up to 6
// levels of imbrication. It is basically impossible to test, maintain, or understand for an // levels of imbrication. It is basically impossible to test, maintain, or understand for an
// outsider. // outsider.
pub(crate) fn new(config: NetworkConfig) -> Self { pub(crate) fn new(config: NetworkConfig, replication_factor: usize) -> Self {
// Create a vector of partition indices (0 to 2**PARTITION_BITS-1) // Create a vector of partition indices (0 to 2**PARTITION_BITS-1)
let partitions_idx = (0usize..(1usize << PARTITION_BITS)).collect::<Vec<_>>(); let partitions_idx = (0usize..(1usize << PARTITION_BITS)).collect::<Vec<_>>();
let datacenters = config let zones = config
.members .members
.iter() .iter()
.map(|(_id, info)| info.datacenter.as_str()) .filter(|(_id, info)| info.capacity.is_some())
.map(|(_id, info)| info.zone.as_str())
.collect::<HashSet<&str>>(); .collect::<HashSet<&str>>();
let n_datacenters = datacenters.len(); let n_zones = zones.len();
// Prepare ring // Prepare ring
let mut partitions: Vec<Vec<(&UUID, &NetworkConfigEntry)>> = partitions_idx let mut partitions: Vec<Vec<(&Uuid, &NetworkConfigEntry)>> = partitions_idx
.iter() .iter()
.map(|_i| Vec::new()) .map(|_i| Vec::new())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -101,6 +152,7 @@ impl Ring {
let mut queues = config let mut queues = config
.members .members
.iter() .iter()
.filter(|(_id, info)| info.capacity.is_some())
.map(|(node_id, node_info)| { .map(|(node_id, node_info)| {
let mut parts = partitions_idx let mut parts = partitions_idx
.iter() .iter()
@@ -119,11 +171,13 @@ impl Ring {
let max_capacity = config let max_capacity = config
.members .members
.iter() .iter()
.map(|(_, node_info)| node_info.capacity) .filter_map(|(_, node_info)| node_info.capacity)
.fold(0, std::cmp::max); .fold(0, std::cmp::max);
assert!(replication_factor <= MAX_REPLICATION);
// Fill up ring // Fill up ring
for rep in 0..MAX_REPLICATION { for rep in 0..replication_factor {
queues.sort_by_key(|(ni, _np, _q, _p)| { queues.sort_by_key(|(ni, _np, _q, _p)| {
let queue_data = [&u16::to_be_bytes(rep as u16)[..], ni.as_slice()].concat(); let queue_data = [&u16::to_be_bytes(rep as u16)[..], ni.as_slice()].concat();
fasthash(&queue_data[..]) fasthash(&queue_data[..])
@@ -138,21 +192,19 @@ impl Ring {
let remaining0 = remaining; let remaining0 = remaining;
for i_round in 0..max_capacity { for i_round in 0..max_capacity {
for (node_id, node_info, q, pos) in queues.iter_mut() { for (node_id, node_info, q, pos) in queues.iter_mut() {
if i_round >= node_info.capacity { if i_round >= node_info.capacity.unwrap() {
continue; continue;
} }
for pos2 in *pos..q.len() { for (pos2, &qv) in q.iter().enumerate().skip(*pos) {
let qv = q[pos2];
if partitions[qv].len() != rep { if partitions[qv].len() != rep {
continue; continue;
} }
let p_dcs = partitions[qv] let p_zns = partitions[qv]
.iter() .iter()
.map(|(_id, info)| info.datacenter.as_str()) .map(|(_id, info)| info.zone.as_str())
.collect::<HashSet<&str>>(); .collect::<HashSet<&str>>();
if (p_dcs.len() < n_datacenters if (p_zns.len() < n_zones && !p_zns.contains(&node_info.zone.as_str()))
&& !p_dcs.contains(&node_info.datacenter.as_str())) || (p_zns.len() == n_zones
|| (p_dcs.len() == n_datacenters
&& !partitions[qv].iter().any(|(id, _i)| id == node_id)) && !partitions[qv].iter().any(|(id, _i)| id == node_id))
{ {
partitions[qv].push((node_id, node_info)); partitions[qv].push((node_id, node_info));
@@ -167,34 +219,58 @@ impl Ring {
// No progress made, exit // No progress made, exit
warn!("Could not build ring, not enough nodes configured."); warn!("Could not build ring, not enough nodes configured.");
return Self { return Self {
replication_factor,
config, config,
nodes: vec![],
ring: vec![], ring: vec![],
}; };
} }
} }
} }
// Make a canonical order for nodes
let nodes = config
.members
.iter()
.filter(|(_id, info)| info.capacity.is_some())
.map(|(id, _)| *id)
.collect::<Vec<_>>();
let nodes_rev = nodes
.iter()
.enumerate()
.map(|(i, id)| (*id, i as CompactNodeType))
.collect::<HashMap<Uuid, CompactNodeType>>();
let ring = partitions let ring = partitions
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, nodes)| { .map(|(i, nodes)| {
let top = (i as u16) << (16 - PARTITION_BITS); let top = (i as u16) << (16 - PARTITION_BITS);
let mut hash = [0u8; 32]; let nodes = nodes
hash[0..2].copy_from_slice(&u16::to_be_bytes(top)[..]); .iter()
let nodes = nodes.iter().map(|(id, _info)| **id).collect::<Vec<UUID>>(); .map(|(id, _info)| *nodes_rev.get(id).unwrap())
.collect::<Vec<CompactNodeType>>();
assert!(nodes.len() == replication_factor);
let mut nodes_buf = [0u8; MAX_REPLICATION];
nodes_buf[..replication_factor].copy_from_slice(&nodes[..]);
RingEntry { RingEntry {
location: hash.into(), hash_prefix: top,
nodes: nodes.try_into().unwrap(), nodes_buf,
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Self { config, ring } Self {
replication_factor,
config,
nodes,
ring,
}
} }
/// Get the partition in which data would fall on /// Get the partition in which data would fall on
pub fn partition_of(&self, from: &Hash) -> Partition { pub fn partition_of(&self, position: &Hash) -> Partition {
let top = u16::from_be_bytes(from.as_slice()[0..2].try_into().unwrap()); let top = u16::from_be_bytes(position.as_slice()[0..2].try_into().unwrap());
top >> (16 - PARTITION_BITS) top >> (16 - PARTITION_BITS)
} }
@@ -203,37 +279,49 @@ impl Ring {
let mut ret = vec![]; let mut ret = vec![];
for (i, entry) in self.ring.iter().enumerate() { for (i, entry) in self.ring.iter().enumerate() {
ret.push((i as u16, entry.location)); let mut location = [0u8; 32];
location[..2].copy_from_slice(&u16::to_be_bytes(entry.hash_prefix)[..]);
ret.push((i as u16, location.into()));
} }
if ret.len() > 0 { if !ret.is_empty() {
assert_eq!(ret[0].1, [0u8; 32].into()); assert_eq!(ret[0].1, [0u8; 32].into());
} }
ret ret
} }
// TODO rename this function as it no longer walk the ring
/// Walk the ring to find the n servers in which data should be replicated /// Walk the ring to find the n servers in which data should be replicated
pub fn walk_ring(&self, from: &Hash, n: usize) -> Vec<UUID> { pub fn get_nodes(&self, position: &Hash, n: usize) -> Vec<Uuid> {
if self.ring.len() != 1 << PARTITION_BITS { if self.ring.len() != 1 << PARTITION_BITS {
warn!("Ring not yet ready, read/writes will be lost!"); warn!("Ring not yet ready, read/writes will be lost!");
return vec![]; return vec![];
} }
let top = u16::from_be_bytes(from.as_slice()[0..2].try_into().unwrap()); let partition_idx = self.partition_of(position) as usize;
let partition_idx = (top >> (16 - PARTITION_BITS)) as usize;
// TODO why computing two time in the same way and asserting?
assert_eq!(partition_idx, self.partition_of(from) as usize);
let partition = &self.ring[partition_idx]; let partition = &self.ring[partition_idx];
let partition_top = let top = u16::from_be_bytes(position.as_slice()[0..2].try_into().unwrap());
u16::from_be_bytes(partition.location.as_slice()[0..2].try_into().unwrap()); // Check that we haven't messed up our partition table, i.e. that this partition
// TODO is this an assertion on the validity of PARTITION_MASK_U16? If so, it should // table entrey indeed corresponds to the item we are storing
// probably be a test more than a runtime assertion assert_eq!(
assert_eq!(partition_top & PARTITION_MASK_U16, top & PARTITION_MASK_U16); partition.hash_prefix & PARTITION_MASK_U16,
top & PARTITION_MASK_U16
);
assert!(n <= partition.nodes.len()); assert!(n <= self.replication_factor);
partition.nodes[..n].iter().cloned().collect::<Vec<_>>() partition.nodes_buf[..n]
.iter()
.map(|i| self.nodes[*i as usize])
.collect::<Vec<_>>()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ring_entry_size() {
assert_eq!(std::mem::size_of::<RingEntry>(), 8);
} }
} }
+20 -20
View File
@@ -19,7 +19,7 @@ use tokio::sync::{watch, Semaphore};
use garage_util::background::BackgroundRunner; use garage_util::background::BackgroundRunner;
use garage_util::config::TlsConfig; use garage_util::config::TlsConfig;
use garage_util::data::*; use garage_util::data::*;
use garage_util::error::{Error, RPCError}; use garage_util::error::{Error, RpcError};
use crate::membership::Status; use crate::membership::Status;
use crate::rpc_server::RpcMessage; use crate::rpc_server::RpcMessage;
@@ -70,7 +70,7 @@ pub struct RpcClient<M: RpcMessage> {
status: watch::Receiver<Arc<Status>>, status: watch::Receiver<Arc<Status>>,
background: Arc<BackgroundRunner>, background: Arc<BackgroundRunner>,
local_handler: ArcSwapOption<(UUID, LocalHandlerFn<M>)>, local_handler: ArcSwapOption<(Uuid, LocalHandlerFn<M>)>,
rpc_addr_client: RpcAddrClient<M>, rpc_addr_client: RpcAddrClient<M>,
} }
@@ -91,7 +91,7 @@ impl<M: RpcMessage + 'static> RpcClient<M> {
} }
/// Set the local handler, to process RPC to this node without network usage /// Set the local handler, to process RPC to this node without network usage
pub fn set_local_handler<F, Fut>(&self, my_id: UUID, handler: F) pub fn set_local_handler<F, Fut>(&self, my_id: Uuid, handler: F)
where where
F: Fn(Arc<M>) -> Fut + Send + Sync + 'static, F: Fn(Arc<M>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<M, Error>> + Send + 'static, Fut: Future<Output = Result<M, Error>> + Send + 'static,
@@ -110,12 +110,12 @@ impl<M: RpcMessage + 'static> RpcClient<M> {
} }
/// Make a RPC call /// Make a RPC call
pub async fn call(&self, to: UUID, msg: M, timeout: Duration) -> Result<M, Error> { pub async fn call(&self, to: Uuid, msg: M, timeout: Duration) -> Result<M, Error> {
self.call_arc(to, Arc::new(msg), timeout).await self.call_arc(to, Arc::new(msg), timeout).await
} }
/// Make a RPC call from a message stored in an Arc /// Make a RPC call from a message stored in an Arc
pub async fn call_arc(&self, to: UUID, msg: Arc<M>, timeout: Duration) -> Result<M, Error> { pub async fn call_arc(&self, to: Uuid, msg: Arc<M>, timeout: Duration) -> Result<M, Error> {
if let Some(lh) = self.local_handler.load_full() { if let Some(lh) = self.local_handler.load_full() {
let (my_id, local_handler) = lh.as_ref(); let (my_id, local_handler) = lh.as_ref();
if to.borrow() == my_id { if to.borrow() == my_id {
@@ -128,7 +128,7 @@ impl<M: RpcMessage + 'static> RpcClient<M> {
if node_status.is_up() { if node_status.is_up() {
node_status node_status
} else { } else {
return Err(Error::from(RPCError::NodeDown(to))); return Err(Error::from(RpcError::NodeDown(to)));
} }
} }
None => { None => {
@@ -152,7 +152,7 @@ impl<M: RpcMessage + 'static> RpcClient<M> {
} }
/// Make a RPC call to multiple servers, returning a Vec containing each result /// Make a RPC call to multiple servers, returning a Vec containing each result
pub async fn call_many(&self, to: &[UUID], msg: M, timeout: Duration) -> Vec<Result<M, Error>> { pub async fn call_many(&self, to: &[Uuid], msg: M, timeout: Duration) -> Vec<Result<M, Error>> {
let msg = Arc::new(msg); let msg = Arc::new(msg);
let mut resp_stream = to let mut resp_stream = to
.iter() .iter()
@@ -170,7 +170,7 @@ impl<M: RpcMessage + 'static> RpcClient<M> {
/// strategy could not be respected due to too many errors /// strategy could not be respected due to too many errors
pub async fn try_call_many( pub async fn try_call_many(
self: &Arc<Self>, self: &Arc<Self>,
to: &[UUID], to: &[Uuid],
msg: M, msg: M,
strategy: RequestStrategy, strategy: RequestStrategy,
) -> Result<Vec<M>, Error> { ) -> Result<Vec<M>, Error> {
@@ -222,7 +222,7 @@ impl<M: RpcMessage + 'static> RpcClient<M> {
Ok(results) Ok(results)
} else { } else {
let errors = errors.iter().map(|e| format!("{}", e)).collect::<Vec<_>>(); let errors = errors.iter().map(|e| format!("{}", e)).collect::<Vec<_>>();
Err(Error::from(RPCError::TooManyErrors(errors))) Err(Error::from(RpcError::TooManyErrors(errors)))
} }
} }
} }
@@ -240,7 +240,7 @@ impl<M: RpcMessage> RpcAddrClient<M> {
pub fn new(http_client: Arc<RpcHttpClient>, path: String) -> Self { pub fn new(http_client: Arc<RpcHttpClient>, path: String) -> Self {
Self { Self {
phantom: PhantomData::default(), phantom: PhantomData::default(),
http_client: http_client, http_client,
path, path,
} }
} }
@@ -251,7 +251,7 @@ impl<M: RpcMessage> RpcAddrClient<M> {
to_addr: &SocketAddr, to_addr: &SocketAddr,
msg: MB, msg: MB,
timeout: Duration, timeout: Duration,
) -> Result<Result<M, Error>, RPCError> ) -> Result<Result<M, Error>, RpcError>
where where
MB: Borrow<M>, MB: Borrow<M>,
{ {
@@ -268,8 +268,8 @@ pub struct RpcHttpClient {
} }
enum ClientMethod { enum ClientMethod {
HTTP(Client<HttpConnector, hyper::Body>), Http(Client<HttpConnector, hyper::Body>),
HTTPS(Client<tls_util::HttpsConnectorFixedDnsname<HttpConnector>, hyper::Body>), Https(Client<tls_util::HttpsConnectorFixedDnsname<HttpConnector>, hyper::Body>),
} }
impl RpcHttpClient { impl RpcHttpClient {
@@ -294,9 +294,9 @@ impl RpcHttpClient {
let connector = let connector =
tls_util::HttpsConnectorFixedDnsname::<HttpConnector>::new(config, "garage"); tls_util::HttpsConnectorFixedDnsname::<HttpConnector>::new(config, "garage");
ClientMethod::HTTPS(Client::builder().build(connector)) ClientMethod::Https(Client::builder().build(connector))
} else { } else {
ClientMethod::HTTP(Client::new()) ClientMethod::Http(Client::new())
}; };
Ok(RpcHttpClient { Ok(RpcHttpClient {
method, method,
@@ -311,14 +311,14 @@ impl RpcHttpClient {
to_addr: &SocketAddr, to_addr: &SocketAddr,
msg: MB, msg: MB,
timeout: Duration, timeout: Duration,
) -> Result<Result<M, Error>, RPCError> ) -> Result<Result<M, Error>, RpcError>
where where
MB: Borrow<M>, MB: Borrow<M>,
M: RpcMessage, M: RpcMessage,
{ {
let uri = match self.method { let uri = match self.method {
ClientMethod::HTTP(_) => format!("http://{}/{}", to_addr, path), ClientMethod::Http(_) => format!("http://{}/{}", to_addr, path),
ClientMethod::HTTPS(_) => format!("https://{}/{}", to_addr, path), ClientMethod::Https(_) => format!("https://{}/{}", to_addr, path),
}; };
let req = Request::builder() let req = Request::builder()
@@ -327,8 +327,8 @@ impl RpcHttpClient {
.body(Body::from(rmp_to_vec_all_named(msg.borrow())?))?; .body(Body::from(rmp_to_vec_all_named(msg.borrow())?))?;
let resp_fut = match &self.method { let resp_fut = match &self.method {
ClientMethod::HTTP(client) => client.request(req).fuse(), ClientMethod::Http(client) => client.request(req).fuse(),
ClientMethod::HTTPS(client) => client.request(req).fuse(), ClientMethod::Https(client) => client.request(req).fuse(),
}; };
trace!("({}) Acquiring request_limiter slot...", path); trace!("({}) Acquiring request_limiter slot...", path);
+4 -4
View File
@@ -57,7 +57,7 @@ where
trace!( trace!(
"Request message: {}", "Request message: {}",
serde_json::to_string(&msg) serde_json::to_string(&msg)
.unwrap_or("<json error>".into()) .unwrap_or_else(|_| "<json error>".into())
.chars() .chars()
.take(100) .take(100)
.collect::<String>() .collect::<String>()
@@ -77,7 +77,7 @@ where
let rep_bytes = rmp_to_vec_all_named::<Result<M, String>>(&Err(err_str))?; let rep_bytes = rmp_to_vec_all_named::<Result<M, String>>(&Err(err_str))?;
let mut err_response = Response::new(Body::from(rep_bytes)); let mut err_response = Response::new(Body::from(rep_bytes));
*err_response.status_mut() = match e { *err_response.status_mut() = match e {
Error::BadRPC(_) => StatusCode::BAD_REQUEST, Error::BadRpc(_) => StatusCode::BAD_REQUEST,
_ => StatusCode::INTERNAL_SERVER_ERROR, _ => StatusCode::INTERNAL_SERVER_ERROR,
}; };
warn!( warn!(
@@ -123,7 +123,7 @@ impl RpcServer {
req: Request<Body>, req: Request<Body>,
addr: SocketAddr, addr: SocketAddr,
) -> Result<Response<Body>, Error> { ) -> Result<Response<Body>, Error> {
if req.method() != &Method::POST { if req.method() != Method::POST {
let mut bad_request = Response::default(); let mut bad_request = Response::default();
*bad_request.status_mut() = StatusCode::BAD_REQUEST; *bad_request.status_mut() = StatusCode::BAD_REQUEST;
return Ok(bad_request); return Ok(bad_request);
@@ -201,7 +201,7 @@ impl RpcServer {
.get_ref() .get_ref()
.0 .0
.peer_addr() .peer_addr()
.unwrap_or(([0, 0, 0, 0], 0).into()); .unwrap_or_else(|_| ([0, 0, 0, 0], 0).into());
let self_arc = self_arc.clone(); let self_arc = self_arc.clone();
async move { async move {
Ok::<_, Error>(service_fn(move |req: Request<Body>| { Ok::<_, Error>(service_fn(move |req: Request<Body>| {
+3 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_table" name = "garage_table"
version = "0.2.1" version = "0.3.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
@@ -13,8 +13,8 @@ path = "lib.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
garage_rpc = { version = "0.2.1", path = "../rpc" } garage_rpc = { version = "0.3.0", path = "../rpc" }
garage_util = { version = "0.2.1", path = "../util" } garage_util = { version = "0.3.0", path = "../util" }
bytes = "1.0" bytes = "1.0"
hexdump = "0.1" hexdump = "0.1"
+1 -1
View File
@@ -27,7 +27,7 @@ impl From<bool> for Bool {
} }
} }
impl CRDT for Bool { impl Crdt for Bool {
fn merge(&mut self, other: &Self) { fn merge(&mut self, other: &Self) {
self.0 = self.0 || other.0; self.0 = self.0 || other.0;
} }
+9 -11
View File
@@ -18,7 +18,7 @@ use garage_util::data::*;
/// Moreover, the relationship `≥` defined by `a ≥ b ⇔ ∃c. a = b ⊔ c` must be a partial order. /// Moreover, the relationship `≥` defined by `a ≥ b ⇔ ∃c. a = b ⊔ c` must be a partial order.
/// This implies a few properties such as: if `a ⊔ b ≠ a`, then there is no `c` such that `(a ⊔ b) ⊔ c = a`, /// This implies a few properties such as: if `a ⊔ b ≠ a`, then there is no `c` such that `(a ⊔ b) ⊔ c = a`,
/// as this would imply a cycle in the partial order. /// as this would imply a cycle in the partial order.
pub trait CRDT { pub trait Crdt {
/// Merge the two datastructures according to the CRDT rules. /// Merge the two datastructures according to the CRDT rules.
/// `self` is modified to contain the merged CRDT value. `other` is not modified. /// `self` is modified to contain the merged CRDT value. `other` is not modified.
/// ///
@@ -31,16 +31,16 @@ pub trait CRDT {
/// All types that implement `Ord` (a total order) can also implement a trivial CRDT /// All types that implement `Ord` (a total order) can also implement a trivial CRDT
/// defined by the merge rule: `a ⊔ b = max(a, b)`. Implement this trait for your type /// defined by the merge rule: `a ⊔ b = max(a, b)`. Implement this trait for your type
/// to enable this behavior. /// to enable this behavior.
pub trait AutoCRDT: Ord + Clone + std::fmt::Debug { pub trait AutoCrdt: Ord + Clone + std::fmt::Debug {
/// WARN_IF_DIFFERENT: emit a warning when values differ. Set this to true if /// WARN_IF_DIFFERENT: emit a warning when values differ. Set this to true if
/// different values in your application should never happen. Set this to false /// different values in your application should never happen. Set this to false
/// if you are actually relying on the semantics of `a ⊔ b = max(a, b)`. /// if you are actually relying on the semantics of `a ⊔ b = max(a, b)`.
const WARN_IF_DIFFERENT: bool; const WARN_IF_DIFFERENT: bool;
} }
impl<T> CRDT for T impl<T> Crdt for T
where where
T: AutoCRDT, T: AutoCrdt,
{ {
fn merge(&mut self, other: &Self) { fn merge(&mut self, other: &Self) {
if Self::WARN_IF_DIFFERENT && self != other { if Self::WARN_IF_DIFFERENT && self != other {
@@ -52,22 +52,20 @@ where
*self = other.clone(); *self = other.clone();
} }
warn!("Making an arbitrary choice: {:?}", self); warn!("Making an arbitrary choice: {:?}", self);
} else { } else if other > self {
if other > self { *self = other.clone();
*self = other.clone();
}
} }
} }
} }
impl AutoCRDT for String { impl AutoCrdt for String {
const WARN_IF_DIFFERENT: bool = true; const WARN_IF_DIFFERENT: bool = true;
} }
impl AutoCRDT for bool { impl AutoCrdt for bool {
const WARN_IF_DIFFERENT: bool = true; const WARN_IF_DIFFERENT: bool = true;
} }
impl AutoCRDT for FixedBytes32 { impl AutoCrdt for FixedBytes32 {
const WARN_IF_DIFFERENT: bool = true; const WARN_IF_DIFFERENT: bool = true;
} }
+5 -5
View File
@@ -36,14 +36,14 @@ use crate::crdt::crdt::*;
/// This scheme is used by AWS S3 or Soundcloud and often without knowing /// This scheme is used by AWS S3 or Soundcloud and often without knowing
/// in enterprise when reconciliating databases with ad-hoc scripts. /// in enterprise when reconciliating databases with ad-hoc scripts.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct LWW<T> { pub struct Lww<T> {
ts: u64, ts: u64,
v: T, v: T,
} }
impl<T> LWW<T> impl<T> Lww<T>
where where
T: CRDT, T: Crdt,
{ {
/// Creates a new CRDT /// Creates a new CRDT
/// ///
@@ -99,9 +99,9 @@ where
} }
} }
impl<T> CRDT for LWW<T> impl<T> Crdt for Lww<T>
where where
T: Clone + CRDT, T: Clone + Crdt,
{ {
fn merge(&mut self, other: &Self) { fn merge(&mut self, other: &Self) {
if other.ts > self.ts { if other.ts > self.ts {
+22 -6
View File
@@ -22,14 +22,14 @@ use crate::crdt::crdt::*;
/// the serialization cost `O(n)` would still have to be paid at each modification, so we are /// the serialization cost `O(n)` would still have to be paid at each modification, so we are
/// actually not losing anything here. /// actually not losing anything here.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct LWWMap<K, V> { pub struct LwwMap<K, V> {
vals: Vec<(K, u64, V)>, vals: Vec<(K, u64, V)>,
} }
impl<K, V> LWWMap<K, V> impl<K, V> LwwMap<K, V>
where where
K: Ord, K: Ord,
V: CRDT, V: Crdt,
{ {
/// Create a new empty map CRDT /// Create a new empty map CRDT
pub fn new() -> Self { pub fn new() -> Self {
@@ -94,7 +94,7 @@ where
/// put_my_crdt_value(a); /// put_my_crdt_value(a);
/// ``` /// ```
pub fn take_and_clear(&mut self) -> Self { pub fn take_and_clear(&mut self) -> Self {
let vals = std::mem::replace(&mut self.vals, vec![]); let vals = std::mem::take(&mut self.vals);
Self { vals } Self { vals }
} }
/// Removes all values from the map /// Removes all values from the map
@@ -113,16 +113,22 @@ where
pub fn items(&self) -> &[(K, u64, V)] { pub fn items(&self) -> &[(K, u64, V)] {
&self.vals[..] &self.vals[..]
} }
/// Returns the number of items in the map /// Returns the number of items in the map
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.vals.len() self.vals.len()
} }
/// Returns true if the map is empty
pub fn is_empty(&self) -> bool {
self.len() == 0
}
} }
impl<K, V> CRDT for LWWMap<K, V> impl<K, V> Crdt for LwwMap<K, V>
where where
K: Clone + Ord, K: Clone + Ord,
V: Clone + CRDT, V: Clone + Crdt,
{ {
fn merge(&mut self, other: &Self) { fn merge(&mut self, other: &Self) {
for (k, ts2, v2) in other.vals.iter() { for (k, ts2, v2) in other.vals.iter() {
@@ -143,3 +149,13 @@ where
} }
} }
} }
impl<K, V> Default for LwwMap<K, V>
where
K: Ord,
V: Crdt,
{
fn default() -> Self {
Self::new()
}
}
+18 -3
View File
@@ -22,7 +22,7 @@ pub struct Map<K, V> {
impl<K, V> Map<K, V> impl<K, V> Map<K, V>
where where
K: Clone + Ord, K: Clone + Ord,
V: Clone + CRDT, V: Clone + Crdt,
{ {
/// Create a new empty map CRDT /// Create a new empty map CRDT
pub fn new() -> Self { pub fn new() -> Self {
@@ -62,12 +62,17 @@ where
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.vals.len() self.vals.len()
} }
/// Returns true if the map is empty
pub fn is_empty(&self) -> bool {
self.len() == 0
}
} }
impl<K, V> CRDT for Map<K, V> impl<K, V> Crdt for Map<K, V>
where where
K: Clone + Ord, K: Clone + Ord,
V: Clone + CRDT, V: Clone + Crdt,
{ {
fn merge(&mut self, other: &Self) { fn merge(&mut self, other: &Self) {
for (k, v2) in other.vals.iter() { for (k, v2) in other.vals.iter() {
@@ -82,3 +87,13 @@ where
} }
} }
} }
impl<K, V> Default for Map<K, V>
where
K: Clone + Ord,
V: Clone + Crdt,
{
fn default() -> Self {
Self::new()
}
}
+1
View File
@@ -10,6 +10,7 @@
//! Learn more about CRDT [on Wikipedia](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) //! Learn more about CRDT [on Wikipedia](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type)
mod bool; mod bool;
#[allow(clippy::module_inception)]
mod crdt; mod crdt;
mod lww; mod lww;
mod lww_map; mod lww_map;
+2 -2
View File
@@ -11,7 +11,7 @@ use garage_util::error::*;
use garage_rpc::membership::System; use garage_rpc::membership::System;
use crate::crdt::CRDT; use crate::crdt::Crdt;
use crate::replication::*; use crate::replication::*;
use crate::schema::*; use crate::schema::*;
@@ -151,7 +151,7 @@ where
if Some(&new_entry) != old_entry.as_ref() { if Some(&new_entry) != old_entry.as_ref() {
let new_bytes = rmp_to_vec_all_named(&new_entry) let new_bytes = rmp_to_vec_all_named(&new_entry)
.map_err(Error::RMPEncode) .map_err(Error::RmpEncode)
.map_err(sled::transaction::ConflictableTransactionError::Abort)?; .map_err(sled::transaction::ConflictableTransactionError::Abort)?;
let new_bytes_hash = blake2sum(&new_bytes[..]); let new_bytes_hash = blake2sum(&new_bytes[..]);
mkl_todo.insert(tree_key.clone(), new_bytes_hash.as_slice())?; mkl_todo.insert(tree_key.clone(), new_bytes_hash.as_slice())?;
+19 -19
View File
@@ -24,23 +24,23 @@ use crate::schema::*;
const TABLE_GC_BATCH_SIZE: usize = 1024; const TABLE_GC_BATCH_SIZE: usize = 1024;
const TABLE_GC_RPC_TIMEOUT: Duration = Duration::from_secs(30); const TABLE_GC_RPC_TIMEOUT: Duration = Duration::from_secs(30);
pub struct TableGC<F: TableSchema, R: TableReplication> { pub struct TableGc<F: TableSchema, R: TableReplication> {
system: Arc<System>, system: Arc<System>,
data: Arc<TableData<F, R>>, data: Arc<TableData<F, R>>,
rpc_client: Arc<RpcClient<GcRPC>>, rpc_client: Arc<RpcClient<GcRpc>>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
enum GcRPC { enum GcRpc {
Update(Vec<ByteBuf>), Update(Vec<ByteBuf>),
DeleteIfEqualHash(Vec<(ByteBuf, Hash)>), DeleteIfEqualHash(Vec<(ByteBuf, Hash)>),
Ok, Ok,
} }
impl RpcMessage for GcRPC {} impl RpcMessage for GcRpc {}
impl<F, R> TableGC<F, R> impl<F, R> TableGc<F, R>
where where
F: TableSchema + 'static, F: TableSchema + 'static,
R: TableReplication + 'static, R: TableReplication + 'static,
@@ -51,7 +51,7 @@ where
rpc_server: &mut RpcServer, rpc_server: &mut RpcServer,
) -> Arc<Self> { ) -> Arc<Self> {
let rpc_path = format!("table_{}/gc", data.name); let rpc_path = format!("table_{}/gc", data.name);
let rpc_client = system.rpc_client::<GcRPC>(&rpc_path); let rpc_client = system.rpc_client::<GcRpc>(&rpc_path);
let gc = Arc::new(Self { let gc = Arc::new(Self {
system: system.clone(), system: system.clone(),
@@ -85,8 +85,8 @@ where
} }
} }
select! { select! {
_ = tokio::time::sleep(Duration::from_secs(10)).fuse() => (), _ = tokio::time::sleep(Duration::from_secs(10)).fuse() => {},
_ = must_exit.changed().fuse() => (), _ = must_exit.changed().fuse() => {},
} }
} }
} }
@@ -120,7 +120,7 @@ where
self.todo_remove_if_equal(&k[..], vhash)?; self.todo_remove_if_equal(&k[..], vhash)?;
} }
if entries.len() == 0 { if entries.is_empty() {
// Nothing to do in this iteration // Nothing to do in this iteration
return Ok(false); return Ok(false);
} }
@@ -168,7 +168,7 @@ where
async fn try_send_and_delete( async fn try_send_and_delete(
&self, &self,
nodes: Vec<UUID>, nodes: Vec<Uuid>,
items: Vec<(ByteBuf, Hash, ByteBuf)>, items: Vec<(ByteBuf, Hash, ByteBuf)>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let n_items = items.len(); let n_items = items.len();
@@ -183,7 +183,7 @@ where
self.rpc_client self.rpc_client
.try_call_many( .try_call_many(
&nodes[..], &nodes[..],
GcRPC::Update(updates), GcRpc::Update(updates),
RequestStrategy::with_quorum(nodes.len()).with_timeout(TABLE_GC_RPC_TIMEOUT), RequestStrategy::with_quorum(nodes.len()).with_timeout(TABLE_GC_RPC_TIMEOUT),
) )
.await?; .await?;
@@ -196,7 +196,7 @@ where
self.rpc_client self.rpc_client
.try_call_many( .try_call_many(
&nodes[..], &nodes[..],
GcRPC::DeleteIfEqualHash(deletes.clone()), GcRpc::DeleteIfEqualHash(deletes.clone()),
RequestStrategy::with_quorum(nodes.len()).with_timeout(TABLE_GC_RPC_TIMEOUT), RequestStrategy::with_quorum(nodes.len()).with_timeout(TABLE_GC_RPC_TIMEOUT),
) )
.await?; .await?;
@@ -221,7 +221,7 @@ where
fn register_handler(self: &Arc<Self>, rpc_server: &mut RpcServer, path: String) { fn register_handler(self: &Arc<Self>, rpc_server: &mut RpcServer, path: String) {
let self2 = self.clone(); let self2 = self.clone();
rpc_server.add_handler::<GcRPC, _, _>(path, move |msg, _addr| { rpc_server.add_handler::<GcRpc, _, _>(path, move |msg, _addr| {
let self2 = self2.clone(); let self2 = self2.clone();
async move { self2.handle_rpc(&msg).await } async move { self2.handle_rpc(&msg).await }
}); });
@@ -234,20 +234,20 @@ where
}); });
} }
async fn handle_rpc(self: &Arc<Self>, message: &GcRPC) -> Result<GcRPC, Error> { async fn handle_rpc(self: &Arc<Self>, message: &GcRpc) -> Result<GcRpc, Error> {
match message { match message {
GcRPC::Update(items) => { GcRpc::Update(items) => {
self.data.update_many(items)?; self.data.update_many(items)?;
Ok(GcRPC::Ok) Ok(GcRpc::Ok)
} }
GcRPC::DeleteIfEqualHash(items) => { GcRpc::DeleteIfEqualHash(items) => {
for (key, vhash) in items.iter() { for (key, vhash) in items.iter() {
self.data.delete_if_equal_hash(&key[..], *vhash)?; self.data.delete_if_equal_hash(&key[..], *vhash)?;
self.todo_remove_if_equal(&key[..], *vhash)?; self.todo_remove_if_equal(&key[..], *vhash)?;
} }
Ok(GcRPC::Ok) Ok(GcRpc::Ok)
} }
_ => Err(Error::Message(format!("Unexpected GC RPC"))), _ => Err(Error::Message("Unexpected GC RPC".to_string())),
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More