mirror of
https://github.com/deuxfleurs-org/garage.git
synced 2026-08-31 17:28:13 +00:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 19a29cce6d | |||
| d65be95051 | |||
| ae2404027a | |||
| 13d313134d | |||
| 05e294307e | |||
| 109fbd49b1 | |||
| d0176f8e30 | |||
| d9b1dba137 | |||
| c3c8af9a66 | |||
| ad78b9ee5c | |||
| b8e844612d | |||
| aa4e4656b1 | |||
| 5549077d6f | |||
| 663fc5ae48 | |||
| 0f89923d2d | |||
| a1f9c52563 | |||
| ceade99cd7 | |||
| d1333c1729 | |||
| 7e246b20e9 | |||
| 936e53fe2f | |||
| bbc1dc3d6f | |||
| 7cdcaa8ebe | |||
| 4133476ec9 | |||
| b277d49ad6 | |||
| 5a4da29f92 | |||
| eb91f463f5 | |||
| 22e003f0d0 | |||
| 0ebf60f709 | |||
| d588a96125 | |||
| e665a3432c | |||
| 5884b4e9c7 | |||
| a32621c8fd | |||
| 8eb7628bf4 | |||
| cd26594673 | |||
| 1b28094f20 | |||
| 4f9faeb282 | |||
| 3c7990027a | |||
| a159c1c483 | |||
| 555e0826a2 | |||
| d0f89068c6 | |||
| 3e25914210 | |||
| 5500f1c412 | |||
| 2d7c0a6087 | |||
| 76aae3cb96 | |||
| 1d1456f1d6 | |||
| a379406522 | |||
| da568cf56f | |||
| 2bde733e09 | |||
| 91573eb028 | |||
| a646180d7e | |||
| bacc6c98b2 | |||
| bf0a24ea69 | |||
| eb37a3e11a |
@@ -53,3 +53,15 @@ steps:
|
||||
- nix-build -j4 --attr flakePackages.dev
|
||||
- nix-shell --attr ci --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||
depends_on: [ build ]
|
||||
|
||||
- name: helm chart tests
|
||||
image: helmunittest/helm-unittest:4.2.3-1.1.2
|
||||
commands:
|
||||
- helm lint --strict script/helm/garage
|
||||
- helm lint --strict script/helm/garage -f script/helm/garage/tests/values/daemonset.yaml
|
||||
- helm lint --strict script/helm/garage -f script/helm/garage/tests/values/ingress.yaml
|
||||
- helm lint --strict script/helm/garage -f script/helm/garage/tests/values/existing-secret.yaml
|
||||
- helm lint --strict script/helm/garage -f script/helm/garage/tests/values/monitoring.yaml
|
||||
- helm lint --strict script/helm/garage -f script/helm/garage/tests/values/minimal.yaml
|
||||
- helm lint --strict script/helm/garage -f script/helm/garage/complex-values.yaml
|
||||
- helm unittest --strict script/helm/garage
|
||||
|
||||
Generated
+43
-71
@@ -1399,6 +1399,30 @@ dependencies = [
|
||||
name = "format_table"
|
||||
version = "0.1.1"
|
||||
|
||||
[[package]]
|
||||
name = "fundu"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ce12752fc64f35be3d53e0a57017cd30970f0cffd73f62c791837d8845badbd"
|
||||
dependencies = [
|
||||
"fundu-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fundu-core"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e463452e2d8b7600d38dcea1ed819773a57f0d710691bfc78db3961bd3f4c3ba"
|
||||
|
||||
[[package]]
|
||||
name = "fundu-systemd"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8641ae8f534f853a552beb3a9f12cc72fd1343260964af82cf09fda66e3d94e"
|
||||
dependencies = [
|
||||
"fundu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.32"
|
||||
@@ -1502,6 +1526,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"crc-fast",
|
||||
"format_table",
|
||||
"fundu-systemd",
|
||||
"futures",
|
||||
"garage_api_admin",
|
||||
"garage_api_common",
|
||||
@@ -1528,7 +1553,6 @@ dependencies = [
|
||||
"opentelemetry",
|
||||
"opentelemetry-otlp",
|
||||
"opentelemetry-prometheus",
|
||||
"parse_duration",
|
||||
"serde_json",
|
||||
"sha1 0.10.6",
|
||||
"sha2 0.10.9",
|
||||
@@ -1749,7 +1773,6 @@ dependencies = [
|
||||
"garage_util",
|
||||
"hex",
|
||||
"http 1.4.0",
|
||||
"parse_duration",
|
||||
"rand 0.9.4",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
@@ -1849,6 +1872,8 @@ dependencies = [
|
||||
"blake2",
|
||||
"bytesize",
|
||||
"chrono",
|
||||
"fundu",
|
||||
"fundu-systemd",
|
||||
"futures",
|
||||
"garage_db",
|
||||
"garage_net",
|
||||
@@ -1881,6 +1906,7 @@ dependencies = [
|
||||
"garage_model",
|
||||
"garage_table",
|
||||
"garage_util",
|
||||
"html-escape",
|
||||
"http 1.4.0",
|
||||
"http-body-util",
|
||||
"hyper 1.9.0",
|
||||
@@ -2194,6 +2220,15 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html-escape"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
|
||||
dependencies = [
|
||||
"utf8-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.12"
|
||||
@@ -3213,41 +3248,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-complex",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.1"
|
||||
@@ -3263,29 +3263,6 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-iter"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -3454,17 +3431,6 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse_duration"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7037e5e93e0172a5a96874380bf73bc6ecef022e26fa25f2be26864d6b3ba95d"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"num",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "password-hash"
|
||||
version = "0.5.0"
|
||||
@@ -5328,6 +5294,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8-width"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
|
||||
+3
-1
@@ -53,10 +53,13 @@ cfg-if = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
crc-fast = "1.9"
|
||||
crypto-common = "0.1"
|
||||
fundu = "2.0"
|
||||
fundu-systemd = "0.3"
|
||||
gethostname = "1.1"
|
||||
git-version = "0.3"
|
||||
hex = "0.4"
|
||||
hexdump = "0.1"
|
||||
html-escape = "0.2.13"
|
||||
hmac = "0.12"
|
||||
itertools = "0.14"
|
||||
ipnet = "2.11"
|
||||
@@ -67,7 +70,6 @@ mktemp = "0.5"
|
||||
nix = { version = "0.31", default-features = false, features = ["fs"] }
|
||||
nom = "8.0"
|
||||
parking_lot = "0.12"
|
||||
parse_duration = "2.1"
|
||||
paste = "1.0"
|
||||
pin-project = "1.1"
|
||||
pnet_datalink = "0.35"
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Governance of Gararge
|
||||
|
||||
This documents how the Garage project operates. It reflects the state of the project as of July 2026 and is not optimal. The team is interested to improve it in the future.
|
||||
|
||||
## Team organization
|
||||
|
||||
* **Contributors**: anyone can contribute by proposing changes in issues and pull requests.
|
||||
|
||||
* **Maintainers**: they are responsible for reviewing, merging pull requests, publishing releases and triaging issues.
|
||||
The current maintainers are:
|
||||
* Alex (handle `lx`)
|
||||
* Trinity (handle `trinity-1686a`)
|
||||
* Quentin (handle `quentin`)
|
||||
* Maximilien (handle `halfa`), who is in particular responsible for coordinating effort on the Kubernetes integration / Helm chart.
|
||||
|
||||
They are added to a white-list of the branch protection rule of the repository to enable them to merge pull requests.
|
||||
To become a maintainer, you need to be a long-term contributor and earn the personal trust of Alex.
|
||||
There is no set process for leaving the maintainer role.
|
||||
|
||||
* **Lead developer**: Alex (handle `lx`) is the lead developer and is responsible of ensuring the
|
||||
correctness of Garage and stability between version upgrades. He may transfer this role to someone else as he sees fit.
|
||||
|
||||
## Communication channels
|
||||
|
||||
The team coordinates in the following channels:
|
||||
* The issue tracker and pull requests of the official repository.
|
||||
* The `#garage:deuxfleurs.fr` matrix channel (in English), open to anyone.
|
||||
On this channel, users may ask for support and discussions about development also happen.
|
||||
* The `#garage-dev:deuxfleurs.fr` matrix channel (in French), not advertised to contributors but de facto accessible to anyone.
|
||||
Discussions about development and project coordination happen there.
|
||||
|
||||
The moderators for those discussion channels are the Garage maintainers.
|
||||
|
||||
## Decision procedures
|
||||
|
||||
Decisions are taken by lazy consensus, with the lead developer settling discussions when a consensus cannot be reached.
|
||||
|
||||
## Governance changes
|
||||
|
||||
There is no set process for changing the governance of garage.
|
||||
|
||||
## See also
|
||||
|
||||
* [Project goals](https://garagehq.deuxfleurs.fr/documentation/design/goals/)
|
||||
* [Contributing instructions](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/main-v2/CONTRIBUTING.md)
|
||||
@@ -547,7 +547,7 @@ ejabberdctl module_install mod_s3_upload
|
||||
Create the required key and bucket with:
|
||||
|
||||
```bash
|
||||
garage key new --name ejabberd
|
||||
garage key create ejabberd
|
||||
garage bucket create objects.xmpp-server.fr
|
||||
garage bucket allow objects.xmpp-server.fr --read --write --key ejabberd
|
||||
garage bucket website --allow objects.xmpp-server.fr
|
||||
@@ -678,7 +678,7 @@ For more information on deployment you can check the [ente documentation](https:
|
||||
This is the usual Garage setup:
|
||||
|
||||
```bash
|
||||
garage key new --name pleroma-key
|
||||
garage key create pleroma-key
|
||||
garage bucket create pleroma
|
||||
garage bucket allow pleroma --read --write --owner --key pleroma-key
|
||||
```
|
||||
@@ -759,7 +759,7 @@ This feature requires `pict-rs >= 4.0.0`.
|
||||
This is the usual Garage setup:
|
||||
|
||||
```bash
|
||||
garage key new --name pictrs-key
|
||||
garage key create pictrs-key
|
||||
garage bucket create pictrs-data
|
||||
garage bucket allow pictrs-data --read --write --key pictrs-key
|
||||
```
|
||||
|
||||
@@ -22,7 +22,7 @@ Note that `git-annex` requires to be compiled with Haskell package version
|
||||
`aws-0.24` to work with Garage.
|
||||
|
||||
```bash
|
||||
garage key new --name my-key
|
||||
garage key create my-key
|
||||
garage bucket create my-git-annex
|
||||
garage bucket allow my-git-annex --read --write --key my-key
|
||||
```
|
||||
|
||||
@@ -268,7 +268,7 @@ duck --delete garage:/my-files/an-object.txt
|
||||
|
||||
## WinSCP (libs3) {#winscp}
|
||||
|
||||
*You can find instructions on how to use the GUI in french [in our wiki](https://guide.deuxfleurs.fr/prise_en_main/winscp/).*
|
||||
*You can find instructions on how to use the GUI in french [in our wiki](https://guide.deuxfleurs.fr/services/winscp/).*
|
||||
|
||||
How to use `winscp.com`, the CLI interface of WinSCP:
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ which support storing metrics in an object store:
|
||||
This can be configured with Garage with the following:
|
||||
|
||||
```bash
|
||||
garage key new --name vector-system-logs
|
||||
garage key create vector-system-logs
|
||||
garage bucket create system-logs
|
||||
garage bucket allow system-logs --read --write --key vector-system-logs
|
||||
```
|
||||
|
||||
@@ -25,6 +25,13 @@ garage bucket website --allow my-website
|
||||
|
||||
Now it will be **publicly** exposed on the web endpoint (by default listening on port 3902).
|
||||
|
||||
> The bucket needs to have a *global alias* to be exposed as a website. If the
|
||||
> bucket was created with `garage bucket create` it will have an alias;
|
||||
> if created via the S3 API [you will have to manually add the alias
|
||||
> ](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/742) first.
|
||||
> Creating globally aliased buckets from the S3 API is [currently under
|
||||
> discussion](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/649).
|
||||
|
||||
## How exposed websites work
|
||||
|
||||
Our website serving logic is as follow:
|
||||
|
||||
@@ -91,7 +91,7 @@ is definitely lost, then there is no other choice than to declare your S3 object
|
||||
as unrecoverable, and to delete them properly from the data store. This can be done
|
||||
using the `garage block purge` command.
|
||||
|
||||
## Rebalancing data directories
|
||||
## Rebalancing data directories {#rebalance}
|
||||
|
||||
In [multi-HDD setups](@/documentation/operations/multi-hdd.md), to ensure that
|
||||
data blocks are well balanced between storage locations, you may run a
|
||||
|
||||
@@ -68,10 +68,11 @@ To rebalance data, two strategies can be used:
|
||||
secondary directory. This might never end up rebalancing everything if there
|
||||
are data blocks that are only read and never written.
|
||||
|
||||
- Active rebalancing: an operator of a Garage node can explicitly launch a repair
|
||||
procedure that rebalances the data directories, moving all blocks to their
|
||||
primary location. Once done, all secondary locations for all hash slices are
|
||||
removed so that they won't be checked anymore when looking for a data block.
|
||||
- Active rebalancing: an operator of a Garage node can [explicitly launch a
|
||||
repair procedure](@/documentation/operations/durability-repairs.md#rebalance)
|
||||
that rebalances the data directories, moving all blocks to their primary
|
||||
location. Once done, all secondary locations for all hash slices are removed
|
||||
so that they won't be checked anymore when looking for a data block.
|
||||
|
||||
## Read-only storage locations
|
||||
|
||||
|
||||
@@ -133,12 +133,17 @@ Use the following command to launch the Garage server:
|
||||
garage server --single-node --default-bucket
|
||||
```
|
||||
|
||||
The `--single-node` flag instructs Garage to automatically configure a single-node cluster without data replication.
|
||||
The `--default-bucket` flag instructs Garage to create a default access key and a default bucket using the environment variables we defined above.
|
||||
Both flags are optional and can be omitted, in which case you will have to follow manual configuration steps described below.
|
||||
- the `--single-node` flag instructs Garage to automatically configure a
|
||||
single-node cluster without data replication;
|
||||
- the `--default-bucket` flag instructs Garage to create a default access key
|
||||
and a default bucket using the environment variables we defined above (it
|
||||
implies `--default-access-key`).
|
||||
|
||||
**For older versions of Garage (before v2.3.0):** automatic configuration using `--single-node` and `--default-bucket` is not available,
|
||||
you must follow the manual configuration steps.
|
||||
> You can refer to the [manual configuration
|
||||
> steps](#manual-configuration) if:
|
||||
>
|
||||
> - you decide to no use these optional flags;
|
||||
> - you are running an **older version of Garage (before v2.3.0)**.
|
||||
|
||||
Alternatively, if you cannot or do not wish to run the Garage binary directly,
|
||||
you may use Docker to run Garage in a container using the following command:
|
||||
@@ -292,7 +297,7 @@ An exhaustive list is maintained in the ["Integrations" > "Browsing tools" secti
|
||||
|
||||
|
||||
|
||||
## Manual configuration
|
||||
## Manual configuration {#manual-configuration}
|
||||
|
||||
This section provides instructions that are equivalent to using the
|
||||
`--single-node` and `--default-bucket` flags for automatic configuration. If
|
||||
|
||||
@@ -175,6 +175,9 @@ they do not exist in the configuration file:
|
||||
Garage daemon send its logs to `journald` (using the native protocol of `systemd-journald`)
|
||||
instead of printing to stderr.
|
||||
|
||||
- `NO_COLOR` (since `v2.4.0`): set this to `0` or `false` to disable
|
||||
ANSI color codes in Garage's logs.
|
||||
|
||||
The following environment variables can be used to override the corresponding
|
||||
values in the configuration file:
|
||||
|
||||
@@ -448,7 +451,7 @@ If this value is set, Garage will automatically take a snapshot of the metadata
|
||||
DB file at a regular interval and save it in the metadata directory,
|
||||
or in [`metadata_snapshots_dir`](#metadata_snapshots_dir) if it is set.
|
||||
This parameter can take any duration string that can be parsed by
|
||||
the [`parse_duration`](https://docs.rs/parse_duration/latest/parse_duration/#syntax) crate.
|
||||
the [`fundu_systemd`](https://docs.rs/fundu-systemd) crate.
|
||||
|
||||
Snapshots can allow to recover from situations where the metadata DB file is
|
||||
corrupted, for instance after an unclean shutdown. See [this
|
||||
|
||||
@@ -8,12 +8,11 @@ which is an alternative storage API designed to help efficiently store
|
||||
many small values in buckets (in opposition to S3 which is more designed
|
||||
to store large blobs).
|
||||
|
||||
K2V is currently disabled at compile time in all builds, as the
|
||||
specification is still subject to changes. To build a Garage version with
|
||||
K2V, the Cargo feature flag `k2v` must be activated. Special builds with
|
||||
the `k2v` feature flag enabled can be obtained from our download page under
|
||||
"Extra builds": such builds can be identified easily as their tag name ends
|
||||
with `-k2v` (example: `v0.7.2-k2v`).
|
||||
K2V is included in release builds since version 0.8.0. Precompiled builds
|
||||
of earlier versions including `k2v` can be found in our download page under
|
||||
"Extra builds": they can be easily identified as their tag name ends with
|
||||
`-k2v` (example: `v0.7.2-k2v`). Otherwise, when compiling Garage, the Cargo
|
||||
feature flag `k2v` must be activated.
|
||||
|
||||
The specification of the K2V API can be found
|
||||
[here](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/commit/f8be15c37db857e177d543de7be863692628d567/doc/drafts/k2v-spec.md).
|
||||
|
||||
@@ -166,6 +166,25 @@ that map to zeroes. Note that we need to filter out values from nodes that are
|
||||
no longer part of the cluster layout, as when nodes are removed they won't
|
||||
necessarily have had the time to set their counters to zero.
|
||||
|
||||
### Consistency guarantees
|
||||
|
||||
K2V provides the following consistency guarantees:
|
||||
|
||||
**Read after Write**. After a write has been acknowledged (the request returned
|
||||
successfully), a subsequent read is guaranteed to contain the value that was
|
||||
written.
|
||||
|
||||
**Monotonic Reads**. Two sequential reads will return values in an order that is
|
||||
consistent with the order in which they are written (e.g. by concurrent writes).
|
||||
For example, consider a scenario where a value is set initially set to 0 and a
|
||||
request writing 1 is performed. Doing two subsequent reads concurrently with the
|
||||
write is guaranteed to return either `0`, `0` or `0`,`1` or `1`,`1`, but not
|
||||
`1`,`0`.
|
||||
|
||||
It is also possible to perform non-monotonic reads (allowing this last
|
||||
behavior), which are slightly faster than monotonic reads. This is done by
|
||||
passing a dedicated flag to read operations (see the endpoints documentation).
|
||||
|
||||
## Important details
|
||||
|
||||
**THIS SECTION CONTAINS A FEW WARNINGS ON THE K2V API WHICH ARE IMPORTANT
|
||||
@@ -210,6 +229,12 @@ Query parameters:
|
||||
|------------|---------------|----------------------------------|
|
||||
| `sort_key` | **mandatory** | The sort key of the item to read |
|
||||
|
||||
Headers:
|
||||
|
||||
| name | default value | meaning |
|
||||
|-------------------------------|---------------|------------------------------------------|
|
||||
| `X-Garage-Non-Monotonic-Read` | `false` | Whether to allow for non-monotonic reads |
|
||||
|
||||
Returns the item with specified partition key and sort key. Values can be
|
||||
returned in either of two ways:
|
||||
|
||||
@@ -325,6 +350,12 @@ Query parameters:
|
||||
|
||||
The timeout can be set to any number of seconds, with a maximum of 600 seconds (10 minutes).
|
||||
|
||||
Headers:
|
||||
|
||||
| name | default value | meaning |
|
||||
|-------------------------------|---------------|------------------------------------------|
|
||||
| `X-Garage-Non-Monotonic-Read` | `false` | Whether to allow for non-monotonic reads |
|
||||
|
||||
|
||||
**InsertItem: `PUT /<bucket>/<partition key>?sort_key=<sort_key>`**
|
||||
|
||||
@@ -521,6 +552,14 @@ HTTP/1.1 204 NO CONTENT
|
||||
|
||||
Batch read of triplets in a bucket.
|
||||
|
||||
Headers:
|
||||
|
||||
| name | default value | meaning |
|
||||
|-------------------------------|---------------|------------------------------------------|
|
||||
| `X-Garage-Non-Monotonic-Read` | `false` | Whether to allow for non-monotonic reads |
|
||||
|
||||
Body:
|
||||
|
||||
The request body is a JSON list of searches, that each specify a range of
|
||||
items to get (to get single items, set `singleItem` to `true`). A search is a
|
||||
JSON struct with the following fields:
|
||||
@@ -711,6 +750,14 @@ HTTP/1.1 200 OK
|
||||
|
||||
Polls a range of items for changes.
|
||||
|
||||
Headers:
|
||||
|
||||
| name | default value | meaning |
|
||||
|-------------------------------|---------------|------------------------------------------|
|
||||
| `X-Garage-Non-Monotonic-Read` | `false` | Whether to allow for non-monotonic reads |
|
||||
|
||||
Body:
|
||||
|
||||
The query body is a JSON object consisting of the following fields:
|
||||
|
||||
| name | default value | meaning |
|
||||
|
||||
+8
-1
@@ -14,7 +14,7 @@ libfuzzer-sys = { workspace = true }
|
||||
garage_db.workspace = true
|
||||
garage_table.workspace = true
|
||||
garage_util.workspace = true
|
||||
garage_model = { workspace = true, default-features = false, features = ["arbitrary"] }
|
||||
garage_model = { workspace = true, default-features = false, features = ["arbitrary", "k2v"] }
|
||||
|
||||
[[bin]]
|
||||
name = "version_crdt"
|
||||
@@ -64,3 +64,10 @@ path = "fuzz_targets/bucket_alias_crdt.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "k2v_item_crdt"
|
||||
path = "fuzz_targets/k2v_item_crdt.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
use garage_fuzz::check_crdt_laws;
|
||||
use garage_model::admin_token_table::{AdminApiToken, AdminApiTokenParams, AdminApiTokenScope};
|
||||
use garage_model::permission::ExpirationTime;
|
||||
use garage_util::crdt;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
type Input = (
|
||||
bool,
|
||||
crdt::Lww<String>,
|
||||
crdt::Lww<Option<u64>>,
|
||||
crdt::Lww<crdt::MergingOption<ExpirationTime>>,
|
||||
crdt::Lww<AdminApiTokenScope>,
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#![no_main]
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use garage_fuzz::check_crdt_laws;
|
||||
use garage_model::k2v::item_table::{DvvsEntry, DvvsValue, K2VItem};
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
// Timestamps are encoded as `(ts << 32) | shift` so that items built with different
|
||||
// shifts (0, 1, 2) have disjoint timestamp spaces that still interleave in the sorted merge.
|
||||
fn make(raw: BTreeMap<u64, (u32, BTreeMap<u32, DvvsValue>)>, shift: u32) -> K2VItem {
|
||||
let shift = shift as u64;
|
||||
let items = raw
|
||||
.into_iter()
|
||||
.map(|(node, (t_discard, values))| {
|
||||
let entry = DvvsEntry::from_raw(
|
||||
(t_discard as u64) << 32 | shift,
|
||||
values
|
||||
.into_iter()
|
||||
.map(|(ts, v)| ((ts as u64) << 32 | shift, v))
|
||||
.collect(),
|
||||
);
|
||||
(node, entry)
|
||||
})
|
||||
.collect();
|
||||
K2VItem::with_raw_items(items)
|
||||
}
|
||||
|
||||
fuzz_target!(|inputs: (
|
||||
BTreeMap<u64, (u32, BTreeMap<u32, DvvsValue>)>,
|
||||
BTreeMap<u64, (u32, BTreeMap<u32, DvvsValue>)>,
|
||||
BTreeMap<u64, (u32, BTreeMap<u32, DvvsValue>)>,
|
||||
)| {
|
||||
let (a, b, c) = inputs;
|
||||
check_crdt_laws(make(a, 0), make(b, 1), make(c, 2));
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use garage_fuzz::check_crdt_laws;
|
||||
use garage_model::key_table::{Key, KeyParams};
|
||||
use garage_model::permission::BucketKeyPerm;
|
||||
use garage_model::permission::{BucketKeyPerm, ExpirationTime};
|
||||
use garage_util::crdt;
|
||||
use garage_util::data::Uuid;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
@@ -10,10 +10,10 @@ use libfuzzer_sys::fuzz_target;
|
||||
type Input = (
|
||||
bool,
|
||||
crdt::Lww<String>,
|
||||
crdt::Lww<Option<u64>>,
|
||||
crdt::Lww<crdt::MergingOption<ExpirationTime>>,
|
||||
crdt::Lww<bool>,
|
||||
crdt::Map<Uuid, BucketKeyPerm>,
|
||||
crdt::LwwMap<String, Option<Uuid>>,
|
||||
crdt::LwwMap<String, crdt::CancelingOption<Uuid>>,
|
||||
);
|
||||
|
||||
fn make(input: Input) -> Key {
|
||||
|
||||
@@ -21,3 +21,5 @@
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
# helm-unittest test suites
|
||||
tests/
|
||||
|
||||
@@ -2,7 +2,7 @@ apiVersion: v2
|
||||
name: garage
|
||||
description: S3-compatible object store for small self-hosted geo-distributed deployments
|
||||
type: application
|
||||
version: 0.9.3
|
||||
version: 0.9.4
|
||||
appVersion: "v2.3.0"
|
||||
home: https://garagehq.deuxfleurs.fr/
|
||||
icon: https://garagehq.deuxfleurs.fr/images/garage-logo.svg
|
||||
@@ -15,4 +15,6 @@ keywords:
|
||||
sources:
|
||||
- https://git.deuxfleurs.fr/Deuxfleurs/garage.git
|
||||
|
||||
maintainers: []
|
||||
maintainers:
|
||||
- name: Garage maintainer team
|
||||
email: garagehq@deuxfleurs.fr
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
# An "everything and the kitchen sink" values file for the helm chart: combines many non-default
|
||||
# settings at once, including examples for the fields that default to empty in
|
||||
# values.yaml and are therefore hard to guess the expected shape of.
|
||||
#
|
||||
# Aside the documentation value, it doubles as an integration-test fixture:
|
||||
# CI renders and lints the chart with this file (see .woodpecker/debug.yaml)
|
||||
# to catch feature interactions that per-feature fixtures wouldn't exercise together
|
||||
# (e.g. both ingresses enabled at once, monitoring + custom service account,
|
||||
# a DaemonSet-incompatible field set alongside a StatefulSet, ...).
|
||||
#
|
||||
# Try it locally with:
|
||||
# helm template script/helm/garage -f script/helm/garage/complex-values.yaml
|
||||
# helm lint --strict script/helm/garage -f script/helm/garage/complex-values.yaml
|
||||
|
||||
# -- Additional labels to add to all resources created by this chart
|
||||
commonLabels:
|
||||
app.kubernetes.io/part-of: storage
|
||||
team: platform-infrastructure
|
||||
|
||||
# Garage configuration. Values under this are written to garage.toml
|
||||
garage:
|
||||
# -- sqlite for durability, lmdb for performance
|
||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#db_engine
|
||||
dbEngine: "sqlite"
|
||||
|
||||
# -- Here set to 10MiB
|
||||
# An increase can result in better performance in certain scenarios
|
||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#block_size
|
||||
blockSize: "10485760"
|
||||
|
||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#replication_factor
|
||||
replicationFactor: "5"
|
||||
|
||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#consistency_mode
|
||||
consistencyMode: "dangerous"
|
||||
|
||||
# -- zstd compression level of stored blocks
|
||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#compression_level
|
||||
compressionLevel: "5"
|
||||
|
||||
# -- If this value is set, Garage will automatically take a snapshot of the metadata DB file and save it in the metadata directory.
|
||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#metadata_auto_snapshot_interval
|
||||
metadataAutoSnapshotInterval: "30 days"
|
||||
|
||||
rpcBindAddr: "[::]:3901"
|
||||
# -- If not given, a random secret will be generated and stored in a Secret object
|
||||
rpcSecret: ""
|
||||
# -- If you want to provide an rpcSecret within an existing k8s secret,
|
||||
# specify the secret name here, and store the value under the secret key `rpcSecret`
|
||||
# ! the default secret will not be created
|
||||
existingRpcSecret: ""
|
||||
# -- This is not required if you use the integrated kubernetes discovery. Each
|
||||
# entry is "<garage_node_id>@<host>:<port>", where <garage_node_id> is the node's public key
|
||||
# (shown by `garage node id` on that node).
|
||||
bootstrapPeers:
|
||||
- "563e1ac825ee3323aa441e72c26d1030d6d4222c43c986812dbf7cd47d18aef@garage-0.garage-headless:3901"
|
||||
- "86f0f26ae4afbd59aaf9cfb302af3fe0464f2f7b5b21f80f7e6f4e9989b5c1f8@garage-1.garage-headless:3901"
|
||||
# -- Set to true if you want to use k8s discovery but install the CRDs manually outside
|
||||
# of the helm chart, for example if you operate at namespace level without cluster resources
|
||||
kubernetesSkipCrd: true
|
||||
s3:
|
||||
api:
|
||||
bindAddr: "[::]:3900"
|
||||
region: "garage"
|
||||
rootDomain: ".s3.garage.tld"
|
||||
web:
|
||||
bindAddr: "[::]:3902"
|
||||
rootDomain: ".web.garage.tld"
|
||||
index: "index.html"
|
||||
admin:
|
||||
apiBindAddr: "[::]:3903"
|
||||
|
||||
# -- Additional configuration to append to garage.toml. Use a multi-line string for custom config.
|
||||
additionalTopLevelConfig: |-
|
||||
data_fsync = true
|
||||
|
||||
# -- if not empty string, allow using an existing ConfigMap for the garage.toml,
|
||||
# if set, ignores garage.toml
|
||||
existingConfigMap: ""
|
||||
|
||||
# -- String Template for the garage configuration.
|
||||
# if set, ignores every other garage.* value above and is rendered with `tpl`,
|
||||
# so it can reference .Values/.Release/.Chart, e.g.:
|
||||
# garageTomlString: |-
|
||||
# metadata_dir = "/mnt/meta"
|
||||
# data_dir = "/mnt/data"
|
||||
# replication_factor = {{ .Values.garage.replicationFactor }}
|
||||
# rpc_bind_addr = "{{ .Values.garage.rpcBindAddr }}"
|
||||
# rpc_secret = "__RPC_SECRET_REPLACE__"
|
||||
# [kubernetes_discovery]
|
||||
# namespace = "{{ .Release.Namespace }}"
|
||||
# service_name = "{{ include "garage.fullname" . }}"
|
||||
# A rendering-verified version of this example lives in tests/configmap_test.yaml.
|
||||
garageTomlString: ""
|
||||
|
||||
# Data persistence
|
||||
persistence:
|
||||
enabled: true
|
||||
meta:
|
||||
storageClass: "fast-ssd"
|
||||
size: 100Mi
|
||||
# used only for daemon sets
|
||||
hostPath: /var/lib/garage/meta
|
||||
data:
|
||||
storageClass: "standard"
|
||||
size: 100Mi
|
||||
# used only for daemon sets
|
||||
hostPath: /var/lib/garage/data
|
||||
|
||||
# Deployment configuration
|
||||
deployment:
|
||||
# -- Switchable to DaemonSet
|
||||
kind: StatefulSet
|
||||
# -- Number of StatefulSet replicas/garage nodes to start
|
||||
replicaCount: 3
|
||||
# -- If using statefulset, allow Parallel or OrderedReady (default)
|
||||
podManagementPolicy: OrderedReady
|
||||
|
||||
image:
|
||||
# -- default to amd64 docker image
|
||||
repository: dxflrs/amd64_garage
|
||||
# -- set the image tag, please prefer using the chart version and not this
|
||||
# to avoid compatibility issues
|
||||
tag: ""
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
initImage:
|
||||
repository: busybox
|
||||
tag: stable
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# -- set if you need credentials to pull your custom image. Each entry needs a
|
||||
# `name:` key, matching a Secret of type kubernetes.io/dockerconfigjson.
|
||||
imagePullSecrets:
|
||||
- name: my-pull-secret
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
serviceAccount:
|
||||
# -- Specifies whether a service account should be created
|
||||
create: true
|
||||
# -- Annotations to add to the service account. Example below is for AWS IRSA.
|
||||
annotations:
|
||||
eks.amazonaws.com/role-arn: "arn:aws:iam::123456789012:role/garage-s3"
|
||||
# -- The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
# -- additional pod annotations
|
||||
podAnnotations:
|
||||
example.com/has-an-annotation: "true"
|
||||
|
||||
podSecurityContext:
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
fsGroup: 1000
|
||||
fsGroupChangePolicy: "OnRootMismatch"
|
||||
runAsNonRoot: true
|
||||
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: true
|
||||
|
||||
service:
|
||||
# -- You can rely on any service to expose your cluster
|
||||
# - ClusterIP (+ Ingress)
|
||||
# - NodePort (+ Ingress)
|
||||
# - LoadBalancer
|
||||
type: ClusterIP
|
||||
# -- Annotations to add to the service. Example below is for an AWS NLB.
|
||||
annotations:
|
||||
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
|
||||
s3:
|
||||
api:
|
||||
port: 3900
|
||||
web:
|
||||
port: 3902
|
||||
# NOTE: the admin API is excluded for now as it is not consistent across nodes
|
||||
|
||||
ingress:
|
||||
s3:
|
||||
api:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
labels: {}
|
||||
hosts:
|
||||
# -- garage S3 API endpoint, to be used with awscli for example
|
||||
- host: "s3.garage.tld"
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
# -- garage S3 API endpoint, DNS style bucket access
|
||||
- host: "*.s3.garage.tld"
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: garage-s3-api-tls
|
||||
hosts:
|
||||
- s3.garage.tld
|
||||
- "*.s3.garage.tld"
|
||||
web:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
labels: {}
|
||||
hosts:
|
||||
# -- wildcard website access with bucket name prefix
|
||||
- host: "*.web.garage.tld"
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
# -- specific bucket access with FQDN bucket
|
||||
- host: "mywebpage.example.com"
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: garage-s3-web-tls
|
||||
hosts:
|
||||
- "*.web.garage.tld"
|
||||
- mywebpage.example.com
|
||||
|
||||
# The following are indicative for a small-size deployment, for anything serious double them.
|
||||
resources:
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 2048Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 1024Mi
|
||||
|
||||
# -- Specifies a livenessProbe
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3903 # or the port from garage.admin.apiBindAddr
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 30
|
||||
# -- Specifies a readinessProbe
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3903 # or the port from garage.admin.apiBindAddr
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 30
|
||||
|
||||
# -- Example: pin pods to a dedicated storage node pool, paired with the
|
||||
# toleration below.
|
||||
nodeSelector:
|
||||
node-role.kubernetes.io/storage: "true"
|
||||
|
||||
tolerations:
|
||||
- key: "dedicated"
|
||||
operator: "Equal"
|
||||
value: "storage"
|
||||
effect: "NoSchedule"
|
||||
|
||||
# -- Example: spread garage replicas across different nodes, since it is a
|
||||
# geo-distributed store that only helps availability if replicas don't share
|
||||
# a failure domain or availability zone.
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
podAffinityTerm:
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: garage
|
||||
topologyKey: kubernetes.io/hostname
|
||||
|
||||
# -- Optional priority class name to assign to the pods.
|
||||
# See https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/
|
||||
# This is expected to reference a PriorityClass you define yourself.
|
||||
priorityClassName: "high-priority-storage"
|
||||
|
||||
# -- Extra container env vars. Note this is a [] of {name, value} objects (ie. a pod env stanza)
|
||||
# GARAGE_ADMIN_TOKEN_FILE below points garage at the token file mounted by
|
||||
# extraVolumes/extraVolumeMounts, see below.
|
||||
environment:
|
||||
- name: RUST_LOG
|
||||
value: "garage=debug"
|
||||
- name: GARAGE_ADMIN_TOKEN_FILE
|
||||
value: /mnt/secrets-store/admin-token
|
||||
|
||||
# -- Extra volumes/volumeMounts. Both are []. Example here mounts the admin API
|
||||
# token from an external secrets manager via the Secrets Store CSI driver
|
||||
# (https://secrets-store-csi-driver.sigs.k8s.io/) instead of a Secret volume.
|
||||
# This allows, for example, providing the tokens without creating a Kubernetes
|
||||
# secret. garage reads the mounted file through GARAGE_ADMIN_TOKEN_FILE above.
|
||||
extraVolumes:
|
||||
- name: secrets-store
|
||||
csi:
|
||||
driver: secrets-store.csi.k8s.io
|
||||
readOnly: true
|
||||
volumeAttributes:
|
||||
secretProviderClass: garage-admin-token
|
||||
extraVolumeMounts:
|
||||
- name: secrets-store
|
||||
mountPath: /mnt/secrets-store
|
||||
readOnly: true
|
||||
|
||||
monitoring:
|
||||
metrics:
|
||||
# -- If true, a service for monitoring is created with a prometheus.io/scrape annotation
|
||||
enabled: true
|
||||
serviceMonitor:
|
||||
# -- If true, a ServiceMonitor CRD is created for a prometheus operator
|
||||
# https://github.com/coreos/prometheus-operator
|
||||
enabled: true
|
||||
path: /metrics
|
||||
# -- Defaults to the namespace the chart is deployed to; this field is
|
||||
# templated, so it can also reference .Release.Namespace itself.
|
||||
namespace: "monitoring"
|
||||
labels:
|
||||
release: prometheus
|
||||
interval: 30s
|
||||
scheme: http
|
||||
tlsConfig: {}
|
||||
scrapeTimeout: 10s
|
||||
relabelings:
|
||||
- sourceLabels: ["__meta_kubernetes_pod_node_name"]
|
||||
targetLabel: node
|
||||
tracing:
|
||||
# -- specify a sink endpoint for OpenTelemetry Traces, eg. `http://localhost:4317`
|
||||
sink: "http://otel-collector.monitoring.svc:4317"
|
||||
@@ -62,7 +62,9 @@ spec:
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if and .Values.ingress.s3.api.enabled .Values.ingress.s3.web.enabled }}
|
||||
---
|
||||
{{ end }}
|
||||
{{- if .Values.ingress.s3.web.enabled -}}
|
||||
{{- $fullName := include "garage.fullname" . -}}
|
||||
{{- $svcPort := .Values.service.s3.web.port -}}
|
||||
|
||||
@@ -28,11 +28,11 @@ spec:
|
||||
scheme: {{ .Values.monitoring.metrics.serviceMonitor.scheme }}
|
||||
{{- with .Values.monitoring.metrics.serviceMonitor.tlsConfig }}
|
||||
tlsConfig:
|
||||
{{- toYaml . | nindent 6 }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.monitoring.metrics.serviceMonitor.relabelings }}
|
||||
relabelings:
|
||||
{{- toYaml . | nindent 6 }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
jobLabel: "{{ .Release.Name }}"
|
||||
selector:
|
||||
@@ -41,4 +41,4 @@ spec:
|
||||
namespaceSelector:
|
||||
matchNames:
|
||||
- {{ .Release.Namespace }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
suite: rbac
|
||||
templates:
|
||||
- templates/clusterrole.yaml
|
||||
tests:
|
||||
- it: allows managing the garage CRD by default
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 2
|
||||
- documentIndex: 0
|
||||
isKind:
|
||||
of: ClusterRole
|
||||
- documentIndex: 0
|
||||
contains:
|
||||
path: rules[0].resources
|
||||
content: customresourcedefinitions
|
||||
- documentIndex: 1
|
||||
isKind:
|
||||
of: ClusterRoleBinding
|
||||
- documentIndex: 1
|
||||
equal:
|
||||
path: subjects[0].name
|
||||
value: RELEASE-NAME-garage
|
||||
|
||||
- it: skips the CRD management rule when the CRD is installed manually
|
||||
set:
|
||||
garage.kubernetesSkipCrd: true
|
||||
asserts:
|
||||
- documentIndex: 0
|
||||
notContains:
|
||||
path: rules[0].resources
|
||||
content: customresourcedefinitions
|
||||
@@ -0,0 +1,82 @@
|
||||
# Integration-style suite: renders the whole chart with complex-values.yaml
|
||||
# (many non-default features combined at once) and checks that they don't
|
||||
# clobber each other, rather than testing any single feature in isolation
|
||||
# (that's what the other tests/*_test.yaml suites are for).
|
||||
suite: complex-values integration
|
||||
templates:
|
||||
- templates/workload.yaml
|
||||
- templates/service.yaml
|
||||
- templates/service-headless.yaml
|
||||
- templates/ingress.yaml
|
||||
- templates/servicemonitor.yaml
|
||||
- templates/serviceaccount.yaml
|
||||
- templates/configmap.yaml
|
||||
- templates/clusterrole.yaml
|
||||
tests:
|
||||
- it: renders a self-consistent deployment with every optional feature enabled
|
||||
values:
|
||||
- ../complex-values.yaml
|
||||
asserts:
|
||||
- template: templates/workload.yaml
|
||||
isKind:
|
||||
of: StatefulSet
|
||||
- template: templates/workload.yaml
|
||||
equal:
|
||||
path: metadata.labels.team
|
||||
value: platform-infrastructure
|
||||
- template: templates/workload.yaml
|
||||
equal:
|
||||
path: spec.template.spec.containers[0].env[0].name
|
||||
value: RUST_LOG
|
||||
- template: templates/workload.yaml
|
||||
contains:
|
||||
path: spec.template.spec.volumes
|
||||
content:
|
||||
name: secrets-store
|
||||
csi:
|
||||
driver: secrets-store.csi.k8s.io
|
||||
readOnly: true
|
||||
volumeAttributes:
|
||||
secretProviderClass: garage-admin-token
|
||||
- template: templates/workload.yaml
|
||||
equal:
|
||||
path: spec.template.spec.containers[0].env[1].name
|
||||
value: GARAGE_ADMIN_TOKEN_FILE
|
||||
- template: templates/workload.yaml
|
||||
equal:
|
||||
path: spec.volumeClaimTemplates[0].spec.storageClassName
|
||||
value: fast-ssd
|
||||
- template: templates/workload.yaml
|
||||
contains:
|
||||
path: spec.template.spec.imagePullSecrets
|
||||
content:
|
||||
name: my-pull-secret
|
||||
- template: templates/service.yaml
|
||||
hasDocuments:
|
||||
count: 2 # main service + metrics service, since monitoring.metrics.enabled is true here
|
||||
- template: templates/service-headless.yaml
|
||||
hasDocuments:
|
||||
count: 1 # StatefulSet still gets a headless service
|
||||
- template: templates/ingress.yaml
|
||||
hasDocuments:
|
||||
count: 2 # both s3 api and s3 web ingresses enabled together
|
||||
- template: templates/servicemonitor.yaml
|
||||
hasDocuments:
|
||||
count: 1
|
||||
- template: templates/servicemonitor.yaml
|
||||
equal:
|
||||
path: spec.endpoints[0].relabelings[0].targetLabel
|
||||
value: node
|
||||
- template: templates/serviceaccount.yaml
|
||||
equal:
|
||||
path: metadata.annotations["eks.amazonaws.com/role-arn"]
|
||||
value: "arn:aws:iam::123456789012:role/garage-s3"
|
||||
- template: templates/configmap.yaml
|
||||
matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'data_fsync = true'
|
||||
- template: templates/clusterrole.yaml
|
||||
documentIndex: 0
|
||||
notContains:
|
||||
path: rules[0].resources
|
||||
content: customresourcedefinitions # garage.kubernetesSkipCrd is true here
|
||||
@@ -0,0 +1,136 @@
|
||||
suite: configmap
|
||||
templates:
|
||||
- templates/configmap.yaml
|
||||
tests:
|
||||
- it: renders garage.toml with the default configuration
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 1
|
||||
- isKind:
|
||||
of: ConfigMap
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: RELEASE-NAME-garage-config
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'metadata_dir = "/mnt/meta"'
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'data_dir = "/mnt/data"'
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'db_engine = "lmdb"'
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'block_size = "1048576"'
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'replication_factor = 3'
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'consistency_mode = "consistent"'
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'compression_level = 1'
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'rpc_bind_addr = "\[::\]:3901"'
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'rpc_secret = "__RPC_SECRET_REPLACE__"'
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: '(?s)\[kubernetes_discovery\]\s*namespace = "NAMESPACE"\s*service_name = "RELEASE-NAME-garage"\s*skip_crd = false'
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: '(?s)\[s3_api\]\s*s3_region = "garage"\s*api_bind_addr = "\[::\]:3900"\s*root_domain = "\.s3\.garage\.tld"'
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: '(?s)\[s3_web\]\s*bind_addr = "\[::\]:3902"\s*root_domain = "\.web\.garage\.tld"\s*index = "index.html"'
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: '(?s)\[admin\]\s*api_bind_addr = "\[::\]:3903"'
|
||||
- notMatchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'metadata_auto_snapshot_interval'
|
||||
- notMatchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'trace_sink'
|
||||
|
||||
- it: reflects custom garage settings, bootstrap peers and additional config
|
||||
set:
|
||||
garage.dbEngine: sqlite
|
||||
garage.blockSize: "2097152"
|
||||
garage.replicationFactor: "5"
|
||||
garage.consistencyMode: degraded
|
||||
garage.compressionLevel: "3"
|
||||
garage.metadataAutoSnapshotInterval: 6h
|
||||
garage.bootstrapPeers:
|
||||
- abc@peer1:3901
|
||||
- def@peer2:3901
|
||||
garage.additionalTopLevelConfig: "data_fsync = true"
|
||||
monitoring.tracing.sink: http://otel:4317
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'db_engine = "sqlite"'
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'block_size = "2097152"'
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'replication_factor = 5'
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'consistency_mode = "degraded"'
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'compression_level = 3'
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'metadata_auto_snapshot_interval = "6h"'
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'bootstrap_peers = \["abc@peer1:3901"\s*, "def@peer2:3901"'
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'data_fsync = true'
|
||||
- matchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'trace_sink = "http://otel:4317"'
|
||||
|
||||
- it: uses garageTomlString verbatim when set, ignoring the structured values
|
||||
set:
|
||||
garage.garageTomlString: |-
|
||||
metadata_dir = "/custom/meta"
|
||||
replication_factor = 1
|
||||
garage.dbEngine: sqlite
|
||||
asserts:
|
||||
- equal:
|
||||
path: data["garage.toml"]
|
||||
value: |-
|
||||
metadata_dir = "/custom/meta"
|
||||
replication_factor = 1
|
||||
- notMatchRegex:
|
||||
path: data["garage.toml"]
|
||||
pattern: 'db_engine'
|
||||
|
||||
- it: templates garageTomlString against the release and values context
|
||||
set:
|
||||
garage.garageTomlString: |-
|
||||
# namespace: {{ .Release.Namespace }}
|
||||
replication_factor = {{ .Values.garage.replicationFactor }}
|
||||
garage.replicationFactor: "7"
|
||||
asserts:
|
||||
- equal:
|
||||
path: data["garage.toml"]
|
||||
value: |-
|
||||
# namespace: NAMESPACE
|
||||
replication_factor = 7
|
||||
|
||||
- it: does not render a ConfigMap when an existing one is referenced
|
||||
set:
|
||||
garage.existingConfigMap: my-external-cm
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
@@ -0,0 +1,95 @@
|
||||
suite: ingress
|
||||
templates:
|
||||
- templates/ingress.yaml
|
||||
tests:
|
||||
- it: renders no ingress by default
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: renders api and web ingresses with tls when enabled
|
||||
values:
|
||||
- ./values/ingress.yaml
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 2
|
||||
- isKind:
|
||||
of: Ingress
|
||||
- documentIndex: 0
|
||||
equal:
|
||||
path: metadata.name
|
||||
value: RELEASE-NAME-garage-s3-api
|
||||
- documentIndex: 0
|
||||
equal:
|
||||
path: spec.ingressClassName
|
||||
value: nginx
|
||||
- documentIndex: 0
|
||||
equal:
|
||||
path: spec.rules[0].host
|
||||
value: s3.example.com
|
||||
- documentIndex: 0
|
||||
equal:
|
||||
path: spec.tls[0].secretName
|
||||
value: garage-s3-api-tls
|
||||
- documentIndex: 1
|
||||
equal:
|
||||
path: metadata.name
|
||||
value: RELEASE-NAME-garage-s3-web
|
||||
- documentIndex: 1
|
||||
equal:
|
||||
path: spec.rules[0].host
|
||||
value: "*.web.example.com"
|
||||
- documentIndex: 1
|
||||
equal:
|
||||
path: spec.tls[0].secretName
|
||||
value: garage-s3-web-tls
|
||||
|
||||
- it: can enable only the s3 api ingress
|
||||
set:
|
||||
ingress.s3.api.enabled: true
|
||||
ingress.s3.api.hosts[0].host: s3.example.com
|
||||
ingress.s3.api.hosts[0].paths[0].path: /
|
||||
ingress.s3.api.hosts[0].paths[0].pathType: Prefix
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 1
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: RELEASE-NAME-garage-s3-api
|
||||
|
||||
- it: omits ingressClassName and tls when neither is configured
|
||||
set:
|
||||
ingress.s3.api.enabled: true
|
||||
ingress.s3.api.hosts[0].host: s3.example.com
|
||||
ingress.s3.api.hosts[0].paths[0].path: /
|
||||
ingress.s3.api.hosts[0].paths[0].pathType: Prefix
|
||||
asserts:
|
||||
- isNull:
|
||||
path: spec.ingressClassName
|
||||
- isNull:
|
||||
path: spec.tls
|
||||
|
||||
- it: renders multiple hosts on the same ingress
|
||||
set:
|
||||
ingress.s3.api.enabled: true
|
||||
ingress.s3.api.hosts:
|
||||
- host: s3.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
- host: s3-alt.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 1
|
||||
- lengthEqual:
|
||||
path: spec.rules
|
||||
count: 2
|
||||
- equal:
|
||||
path: spec.rules[0].host
|
||||
value: s3.example.com
|
||||
- equal:
|
||||
path: spec.rules[1].host
|
||||
value: s3-alt.example.com
|
||||
@@ -0,0 +1,56 @@
|
||||
suite: naming and common labels
|
||||
templates:
|
||||
- templates/workload.yaml
|
||||
- templates/configmap.yaml
|
||||
tests:
|
||||
- it: applies commonLabels alongside the default chart labels
|
||||
template: templates/workload.yaml
|
||||
set:
|
||||
commonLabels:
|
||||
team: storage
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.labels.team
|
||||
value: storage
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/managed-by"]
|
||||
value: Helm
|
||||
|
||||
- it: uses fullnameOverride verbatim for resource names
|
||||
template: templates/workload.yaml
|
||||
set:
|
||||
fullnameOverride: my-garage-cluster
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: my-garage-cluster
|
||||
- equal:
|
||||
path: spec.serviceName
|
||||
value: my-garage-cluster-headless
|
||||
|
||||
- it: does not double-prefix when the release name already contains the chart name
|
||||
template: templates/workload.yaml
|
||||
release:
|
||||
name: garage
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: garage
|
||||
|
||||
- it: prefixes the release name with the chart name otherwise
|
||||
template: templates/workload.yaml
|
||||
release:
|
||||
name: prod
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: prod-garage
|
||||
|
||||
- it: truncates an overly long fullname to 63 characters and trims a trailing dash
|
||||
template: templates/workload.yaml
|
||||
set:
|
||||
fullnameOverride: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-suffix-that-will-be-cut-off
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
@@ -0,0 +1,33 @@
|
||||
suite: rpc secret
|
||||
templates:
|
||||
- templates/secret.yaml
|
||||
tests:
|
||||
- it: generates a Secret holding the rpc secret by default
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 1
|
||||
- isKind:
|
||||
of: Secret
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: RELEASE-NAME-garage-rpc-secret
|
||||
- equal:
|
||||
path: type
|
||||
value: Opaque
|
||||
- isNotNull:
|
||||
path: data.rpcSecret
|
||||
|
||||
- it: does not render a Secret when an existing one is referenced
|
||||
values:
|
||||
- ./values/existing-secret.yaml
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: base64-encodes an explicitly provided rpc secret
|
||||
set:
|
||||
garage.rpcSecret: my-plain-secret
|
||||
asserts:
|
||||
- equal:
|
||||
path: data.rpcSecret
|
||||
value: bXktcGxhaW4tc2VjcmV0
|
||||
@@ -0,0 +1,26 @@
|
||||
suite: headless service
|
||||
templates:
|
||||
- templates/service-headless.yaml
|
||||
tests:
|
||||
- it: creates a headless service for a StatefulSet by default
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 1
|
||||
- isKind:
|
||||
of: Service
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: RELEASE-NAME-garage-headless
|
||||
- equal:
|
||||
path: spec.clusterIP
|
||||
value: None
|
||||
- equal:
|
||||
path: spec.type
|
||||
value: ClusterIP
|
||||
|
||||
- it: does not create a headless service for a DaemonSet
|
||||
values:
|
||||
- ./values/daemonset.yaml
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
@@ -0,0 +1,61 @@
|
||||
suite: service
|
||||
templates:
|
||||
- templates/service.yaml
|
||||
tests:
|
||||
- it: creates a ClusterIP service with s3-api and s3-web ports by default
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 1
|
||||
- isKind:
|
||||
of: Service
|
||||
- equal:
|
||||
path: spec.type
|
||||
value: ClusterIP
|
||||
- equal:
|
||||
path: spec.ports[0].name
|
||||
value: s3-api
|
||||
- equal:
|
||||
path: spec.ports[0].port
|
||||
value: 3900
|
||||
- equal:
|
||||
path: spec.ports[1].name
|
||||
value: s3-web
|
||||
- equal:
|
||||
path: spec.ports[1].port
|
||||
value: 3902
|
||||
|
||||
- it: honors a custom service type and port
|
||||
set:
|
||||
service.type: LoadBalancer
|
||||
service.s3.api.port: 9000
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.type
|
||||
value: LoadBalancer
|
||||
- equal:
|
||||
path: spec.ports[0].port
|
||||
value: 9000
|
||||
|
||||
- it: does not create a metrics service by default
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 1
|
||||
|
||||
- it: adds a headless metrics service when monitoring is enabled
|
||||
values:
|
||||
- ./values/monitoring.yaml
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 2
|
||||
- documentIndex: 1
|
||||
equal:
|
||||
path: metadata.name
|
||||
value: RELEASE-NAME-garage-metrics
|
||||
- documentIndex: 1
|
||||
equal:
|
||||
path: spec.clusterIP
|
||||
value: None
|
||||
- documentIndex: 1
|
||||
equal:
|
||||
path: metadata.annotations["prometheus.io/scrape"]
|
||||
value: "true"
|
||||
@@ -0,0 +1,28 @@
|
||||
suite: service account
|
||||
templates:
|
||||
- templates/serviceaccount.yaml
|
||||
tests:
|
||||
- it: creates a ServiceAccount by default
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 1
|
||||
- isKind:
|
||||
of: ServiceAccount
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: RELEASE-NAME-garage
|
||||
|
||||
- it: does not create a ServiceAccount when disabled
|
||||
values:
|
||||
- ./values/minimal.yaml
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: honors a custom service account name
|
||||
set:
|
||||
serviceAccount.name: my-garage-sa
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: my-garage-sa
|
||||
@@ -0,0 +1,65 @@
|
||||
suite: service monitor
|
||||
templates:
|
||||
- templates/servicemonitor.yaml
|
||||
tests:
|
||||
- it: renders no ServiceMonitor by default
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: renders no ServiceMonitor when only metrics are enabled
|
||||
set:
|
||||
monitoring.metrics.enabled: true
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: renders a ServiceMonitor when explicitly enabled
|
||||
values:
|
||||
- ./values/monitoring.yaml
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 1
|
||||
- isKind:
|
||||
of: ServiceMonitor
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: RELEASE-NAME-garage
|
||||
- equal:
|
||||
path: metadata.namespace
|
||||
value: NAMESPACE
|
||||
- equal:
|
||||
path: spec.endpoints[0].interval
|
||||
value: 30s
|
||||
|
||||
- it: templates a custom namespace against the release context
|
||||
values:
|
||||
- ./values/monitoring.yaml
|
||||
set:
|
||||
monitoring.metrics.serviceMonitor.namespace: "{{ .Release.Namespace }}-monitoring"
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.namespace
|
||||
value: NAMESPACE-monitoring
|
||||
|
||||
- it: applies custom labels, tlsConfig and relabelings
|
||||
values:
|
||||
- ./values/monitoring.yaml
|
||||
set:
|
||||
monitoring.metrics.serviceMonitor.labels:
|
||||
team: storage
|
||||
monitoring.metrics.serviceMonitor.tlsConfig:
|
||||
insecureSkipVerify: true
|
||||
monitoring.metrics.serviceMonitor.relabelings:
|
||||
- sourceLabels: ["__meta_kubernetes_pod_name"]
|
||||
targetLabel: pod
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.labels.team
|
||||
value: storage
|
||||
- equal:
|
||||
path: spec.endpoints[0].tlsConfig.insecureSkipVerify
|
||||
value: true
|
||||
- equal:
|
||||
path: spec.endpoints[0].relabelings[0].targetLabel
|
||||
value: pod
|
||||
@@ -0,0 +1,10 @@
|
||||
# Run garage as a DaemonSet (one pod per node) instead of the default StatefulSet,
|
||||
# using hostPath volumes for meta/data persistence.
|
||||
deployment:
|
||||
kind: DaemonSet
|
||||
persistence:
|
||||
enabled: true
|
||||
meta:
|
||||
hostPath: /var/lib/garage/meta
|
||||
data:
|
||||
hostPath: /var/lib/garage/data
|
||||
@@ -0,0 +1,5 @@
|
||||
# Use a pre-existing Kubernetes Secret for the RPC secret instead of letting
|
||||
# the chart generate/manage one.
|
||||
garage:
|
||||
rpcSecret: ""
|
||||
existingRpcSecret: "garage-rpc-secret-external"
|
||||
@@ -0,0 +1,27 @@
|
||||
# Expose both the S3 API and website endpoints through Ingress, with TLS.
|
||||
ingress:
|
||||
s3:
|
||||
api:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
hosts:
|
||||
- host: "s3.example.com"
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: garage-s3-api-tls
|
||||
hosts:
|
||||
- s3.example.com
|
||||
web:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
hosts:
|
||||
- host: "*.web.example.com"
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: garage-s3-web-tls
|
||||
hosts:
|
||||
- "*.web.example.com"
|
||||
@@ -0,0 +1,8 @@
|
||||
# Minimal single-node deployment without persistent storage or a dedicated
|
||||
# service account, e.g. for local testing.
|
||||
deployment:
|
||||
replicaCount: 1
|
||||
persistence:
|
||||
enabled: false
|
||||
serviceAccount:
|
||||
create: false
|
||||
@@ -0,0 +1,7 @@
|
||||
# Enable Prometheus metrics scraping and a ServiceMonitor for the prometheus-operator.
|
||||
monitoring:
|
||||
metrics:
|
||||
enabled: true
|
||||
serviceMonitor:
|
||||
enabled: true
|
||||
interval: 30s
|
||||
@@ -0,0 +1,187 @@
|
||||
suite: workload (StatefulSet/DaemonSet)
|
||||
templates:
|
||||
- templates/workload.yaml
|
||||
- templates/configmap.yaml
|
||||
tests:
|
||||
- it: defaults to a StatefulSet with 3 replicas and 2 volumes
|
||||
template: templates/workload.yaml
|
||||
asserts:
|
||||
- isKind:
|
||||
of: StatefulSet
|
||||
- equal:
|
||||
path: spec.replicas
|
||||
value: 3
|
||||
- equal:
|
||||
path: spec.podManagementPolicy
|
||||
value: OrderedReady
|
||||
- equal:
|
||||
path: spec.template.spec.volumes[1].name
|
||||
value: etc
|
||||
- lengthEqual:
|
||||
path: spec.template.spec.volumes
|
||||
count: 2
|
||||
- isNotNull:
|
||||
path: spec.volumeClaimTemplates
|
||||
|
||||
- it: uses a StatefulSet with PVC-backed volumeClaimTemplates by default
|
||||
template: templates/workload.yaml
|
||||
asserts:
|
||||
- isKind:
|
||||
of: StatefulSet
|
||||
- isNotNull:
|
||||
path: spec.volumeClaimTemplates
|
||||
- equal:
|
||||
path: spec.volumeClaimTemplates[0].spec.resources.requests.storage
|
||||
value: 100Mi
|
||||
|
||||
- it: switches to a DaemonSet with hostPath volumes when requested
|
||||
template: templates/workload.yaml
|
||||
values:
|
||||
- ./values/daemonset.yaml
|
||||
asserts:
|
||||
- isKind:
|
||||
of: DaemonSet
|
||||
- isNull:
|
||||
path: spec.replicas
|
||||
- isNull:
|
||||
path: spec.volumeClaimTemplates
|
||||
- contains:
|
||||
path: spec.template.spec.volumes
|
||||
content:
|
||||
name: meta
|
||||
hostPath:
|
||||
path: /var/lib/garage/meta
|
||||
type: DirectoryOrCreate
|
||||
- contains:
|
||||
path: spec.template.spec.volumes
|
||||
content:
|
||||
name: data
|
||||
hostPath:
|
||||
path: /var/lib/garage/data
|
||||
type: DirectoryOrCreate
|
||||
|
||||
- it: renders emptyDir volumes when persistence is disabled
|
||||
template: templates/workload.yaml
|
||||
values:
|
||||
- ./values/minimal.yaml
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.volumes
|
||||
content:
|
||||
name: meta
|
||||
emptyDir: {}
|
||||
- contains:
|
||||
path: spec.template.spec.volumes
|
||||
content:
|
||||
name: data
|
||||
emptyDir: {}
|
||||
- isNull:
|
||||
path: spec.volumeClaimTemplates
|
||||
|
||||
- it: honors a custom replicaCount
|
||||
template: templates/workload.yaml
|
||||
set:
|
||||
deployment.replicaCount: 5
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.replicas
|
||||
value: 5
|
||||
|
||||
- it: points the init container at the configured rpc secret
|
||||
template: templates/workload.yaml
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.initContainers[0].env[0].valueFrom.secretKeyRef.name
|
||||
value: RELEASE-NAME-garage-rpc-secret
|
||||
|
||||
- it: points the init container at an existing rpc secret when configured
|
||||
template: templates/workload.yaml
|
||||
values:
|
||||
- ./values/existing-secret.yaml
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.initContainers[0].env[0].valueFrom.secretKeyRef.name
|
||||
value: garage-rpc-secret-external
|
||||
|
||||
- it: sets the container image from repository and tag
|
||||
template: templates/workload.yaml
|
||||
set:
|
||||
image.repository: dxflrs/amd64_garage
|
||||
image.tag: v1.2.3
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.containers[0].image
|
||||
value: dxflrs/amd64_garage:v1.2.3
|
||||
|
||||
- it: falls back to the chart appVersion when no image tag is set
|
||||
template: templates/workload.yaml
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: spec.template.spec.containers[0].image
|
||||
pattern: ^dxflrs/amd64_garage:v
|
||||
|
||||
- it: omits storageClassName from volumeClaimTemplates by default
|
||||
template: templates/workload.yaml
|
||||
asserts:
|
||||
- isNull:
|
||||
path: spec.volumeClaimTemplates[0].spec.storageClassName
|
||||
- isNull:
|
||||
path: spec.volumeClaimTemplates[1].spec.storageClassName
|
||||
|
||||
- it: sets storageClassName in volumeClaimTemplates when configured
|
||||
template: templates/workload.yaml
|
||||
set:
|
||||
persistence.meta.storageClass: fast-storage
|
||||
persistence.data.storageClass: slow-storage
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.volumeClaimTemplates[0].spec.storageClassName
|
||||
value: fast-storage
|
||||
- equal:
|
||||
path: spec.volumeClaimTemplates[1].spec.storageClassName
|
||||
value: slow-storage
|
||||
|
||||
- it: renders emptyDir volumes for a DaemonSet when persistence is disabled
|
||||
template: templates/workload.yaml
|
||||
set:
|
||||
deployment.kind: DaemonSet
|
||||
persistence.enabled: false
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.volumes
|
||||
content:
|
||||
name: meta
|
||||
emptyDir: {}
|
||||
- contains:
|
||||
path: spec.template.spec.volumes
|
||||
content:
|
||||
name: data
|
||||
emptyDir: {}
|
||||
|
||||
- it: mounts the existing ConfigMap volume when configured
|
||||
template: templates/workload.yaml
|
||||
set:
|
||||
garage.existingConfigMap: my-external-cm
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.volumes[0].configMap.name
|
||||
value: my-external-cm
|
||||
|
||||
- it: uses a custom service account name without creating one when disabled
|
||||
template: templates/workload.yaml
|
||||
set:
|
||||
serviceAccount.create: false
|
||||
serviceAccount.name: my-external-sa
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.serviceAccountName
|
||||
value: my-external-sa
|
||||
|
||||
- it: falls back to the default service account when disabled without a custom name
|
||||
template: templates/workload.yaml
|
||||
set:
|
||||
serviceAccount.create: false
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.serviceAccountName
|
||||
value: default
|
||||
@@ -246,10 +246,14 @@ affinity: {}
|
||||
# See https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/
|
||||
priorityClassName: ""
|
||||
|
||||
# -- Extra container env vars, as a list of {name, value} objects (same shape
|
||||
# as a Pod container's env)
|
||||
environment: {}
|
||||
|
||||
# -- Extra volumes, as a list of volume objects (same shape as a PodSpec's volumes)
|
||||
extraVolumes: {}
|
||||
|
||||
# -- Extra volume mounts, as a list of mount objects (same shape as a container's volumeMounts)
|
||||
extraVolumeMounts: {}
|
||||
|
||||
monitoring:
|
||||
|
||||
@@ -7,6 +7,7 @@ use garage_util::time::now_msec;
|
||||
|
||||
use garage_model::admin_token_table::*;
|
||||
use garage_model::garage::Garage;
|
||||
use garage_model::permission::ExpirationTime;
|
||||
|
||||
use crate::api::*;
|
||||
use crate::error::*;
|
||||
@@ -244,8 +245,8 @@ fn admin_token_info_results(token: &AdminApiToken, now: u64) -> GetAdminTokenInf
|
||||
.expect("invalid timestamp stored in db"),
|
||||
),
|
||||
name: params.name.get().to_string(),
|
||||
expiration: params.expiration.get().map(|x| {
|
||||
DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db")
|
||||
expiration: params.expiration.get().inner().map(|x| {
|
||||
DateTime::from_timestamp_millis(x.0 as i64).expect("invalid timestamp stored in db")
|
||||
}),
|
||||
expired: params.is_expired(now),
|
||||
scope: params.scope.get().0.clone(),
|
||||
@@ -279,10 +280,10 @@ fn apply_token_updates(
|
||||
if let Some(expiration) = updates.expiration {
|
||||
params
|
||||
.expiration
|
||||
.update(Some(expiration.timestamp_millis() as u64));
|
||||
.update(Some(ExpirationTime(expiration.timestamp_millis() as u64)).into());
|
||||
}
|
||||
if updates.never_expires {
|
||||
params.expiration.update(None);
|
||||
params.expiration.update(None.into());
|
||||
}
|
||||
if let Some(scope) = updates.scope {
|
||||
params.scope.update(AdminApiTokenScope(scope));
|
||||
|
||||
+58
-63
@@ -90,7 +90,7 @@ impl RequestHandler for GetBucketInfoRequest {
|
||||
.bucket_alias_table
|
||||
.get(&EmptyKey, &ga)
|
||||
.await?
|
||||
.and_then(|x| *x.state.get())
|
||||
.and_then(|x| x.state.get().into_inner())
|
||||
.ok_or_else(|| HelperError::NoSuchBucket(ga.to_string()))?,
|
||||
(None, None, Some(search)) => {
|
||||
let helper = garage.bucket_helper();
|
||||
@@ -168,7 +168,7 @@ impl RequestHandler for CreateBucketRequest {
|
||||
}
|
||||
|
||||
if let Some(alias) = garage.bucket_alias_table.get(&EmptyKey, ga).await? {
|
||||
if alias.state.get().is_some() {
|
||||
if alias.state.get().inner().is_some() {
|
||||
return Err(CommonError::BucketAlreadyExists.into());
|
||||
}
|
||||
}
|
||||
@@ -297,7 +297,7 @@ impl RequestHandler for UpdateBucketRequest {
|
||||
let redirect_all = state
|
||||
.website_config
|
||||
.get()
|
||||
.as_ref()
|
||||
.inner()
|
||||
.and_then(|wc| wc.redirect_all.clone());
|
||||
|
||||
let routing_rules = if let Some(rr) = wa.routing_rules {
|
||||
@@ -311,26 +311,29 @@ impl RequestHandler for UpdateBucketRequest {
|
||||
state
|
||||
.website_config
|
||||
.get()
|
||||
.as_ref()
|
||||
.inner()
|
||||
.map(|wc| wc.routing_rules.clone())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
state.website_config.update(Some(WebsiteConfig {
|
||||
index_document: wa.index_document.ok_or_bad_request(
|
||||
"Please specify indexDocument when enabling website access.",
|
||||
)?,
|
||||
error_document: wa.error_document,
|
||||
redirect_all,
|
||||
routing_rules,
|
||||
}));
|
||||
state.website_config.update(
|
||||
Some(WebsiteConfig {
|
||||
index_document: wa.index_document.ok_or_bad_request(
|
||||
"Please specify indexDocument when enabling website access.",
|
||||
)?,
|
||||
error_document: wa.error_document,
|
||||
redirect_all,
|
||||
routing_rules,
|
||||
})
|
||||
.into(),
|
||||
);
|
||||
} else {
|
||||
if wa.index_document.is_some() || wa.error_document.is_some() {
|
||||
return Err(Error::bad_request(
|
||||
"Cannot specify indexDocument or errorDocument when disabling website access.",
|
||||
));
|
||||
}
|
||||
state.website_config.update(None);
|
||||
state.website_config.update(None.into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +356,7 @@ impl RequestHandler for UpdateBucketRequest {
|
||||
Some(cc.into_garage_cors_config()?)
|
||||
};
|
||||
|
||||
state.cors_config.update(cors_config);
|
||||
state.cors_config.update(cors_config.into());
|
||||
}
|
||||
|
||||
if let Some(lr) = self.body.lifecycle_rules {
|
||||
@@ -370,7 +373,7 @@ impl RequestHandler for UpdateBucketRequest {
|
||||
)
|
||||
};
|
||||
|
||||
state.lifecycle_config.update(lifecycle_config);
|
||||
state.lifecycle_config.update(lifecycle_config.into());
|
||||
}
|
||||
|
||||
garage.bucket_table.insert(&bucket).await?;
|
||||
@@ -687,45 +690,36 @@ async fn bucket_info_results(
|
||||
.map(|x| x.filtered_values(&garage.system.cluster_layout()))
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut relevant_keys = HashMap::new();
|
||||
for (k, _) in bucket
|
||||
.state
|
||||
.as_option()
|
||||
.unwrap()
|
||||
let state = bucket.state.as_option().unwrap();
|
||||
|
||||
let keys1 = state
|
||||
.authorized_keys
|
||||
.items()
|
||||
.iter()
|
||||
{
|
||||
if let Some(key) = garage
|
||||
.key_table
|
||||
.get(&EmptyKey, k)
|
||||
.await?
|
||||
.filter(|k| !k.is_deleted())
|
||||
{
|
||||
if !key.state.is_deleted() {
|
||||
relevant_keys.insert(k.clone(), key);
|
||||
}
|
||||
}
|
||||
}
|
||||
for ((k, _), _, _) in bucket
|
||||
.state
|
||||
.as_option()
|
||||
.unwrap()
|
||||
.filter(|(_, p)| p.is_any())
|
||||
.map(|(k, _)| k);
|
||||
let keys2 = state
|
||||
.local_aliases
|
||||
.items()
|
||||
.iter()
|
||||
{
|
||||
if relevant_keys.contains_key(k) {
|
||||
.filter(|(_, _, p)| *p)
|
||||
.map(|((k, _), _, _)| k);
|
||||
|
||||
let mut relevant_keys = HashMap::new();
|
||||
for key_id in keys1.chain(keys2) {
|
||||
if relevant_keys.contains_key(key_id) {
|
||||
continue;
|
||||
}
|
||||
if let Some(key) = garage.key_table.get(&EmptyKey, k).await? {
|
||||
if !key.state.is_deleted() {
|
||||
relevant_keys.insert(k.clone(), key);
|
||||
}
|
||||
if let Some(key) = garage.key_table.get(&EmptyKey, key_id).await? {
|
||||
relevant_keys.insert(key_id.clone(), key);
|
||||
} else {
|
||||
warn!(
|
||||
"Bucket {:?} references non-existent key {}",
|
||||
bucket.id, key_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let state = bucket.state.as_option().unwrap();
|
||||
relevant_keys.retain(|_, k| !k.is_deleted());
|
||||
|
||||
let quotas = state.quotas.get();
|
||||
let res = GetBucketInfoResponse {
|
||||
@@ -739,8 +733,8 @@ async fn bucket_info_results(
|
||||
.filter(|(_, _, a)| *a)
|
||||
.map(|(n, _, _)| n.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
website_access: state.website_config.get().is_some(),
|
||||
website_config: state.website_config.get().clone().map(|wsc| {
|
||||
website_access: state.website_config.get().inner().is_some(),
|
||||
website_config: state.website_config.get().inner().cloned().map(|wsc| {
|
||||
GetBucketInfoWebsiteResponse {
|
||||
index_document: wsc.index_document,
|
||||
error_document: wsc.error_document,
|
||||
@@ -752,42 +746,43 @@ async fn bucket_info_results(
|
||||
),
|
||||
}
|
||||
}),
|
||||
cors_rules: state.cors_config.get().as_ref().map(|rules| {
|
||||
cors_rules: state.cors_config.get().inner().map(|rules| {
|
||||
rules
|
||||
.iter()
|
||||
.map(xml::cors::CorsRule::from_garage_cors_rule)
|
||||
.collect::<Vec<_>>()
|
||||
}),
|
||||
lifecycle_rules: state.lifecycle_config.get().as_ref().map(|lc| {
|
||||
lifecycle_rules: state.lifecycle_config.get().inner().map(|lc| {
|
||||
lc.iter()
|
||||
.map(xml::lifecycle::LifecycleRule::from_garage_lifecycle_rule)
|
||||
.collect::<Vec<_>>()
|
||||
}),
|
||||
keys: relevant_keys
|
||||
.into_values()
|
||||
.filter_map(|key| {
|
||||
let p = key.state.as_option().unwrap();
|
||||
let permissions = p
|
||||
.map(|key| {
|
||||
let st = key.state.as_option().unwrap();
|
||||
let permissions = st
|
||||
.authorized_buckets
|
||||
.get(&bucket.id)
|
||||
.filter(|p| p.is_any())
|
||||
.map(|p| ApiBucketKeyPerm {
|
||||
read: p.allow_read,
|
||||
write: p.allow_write,
|
||||
owner: p.allow_owner,
|
||||
})?;
|
||||
Some(GetBucketInfoKey {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let bucket_local_aliases = st
|
||||
.local_aliases
|
||||
.items()
|
||||
.iter()
|
||||
.filter(|(_, _, b)| b.into_inner() == Some(bucket.id))
|
||||
.map(|(n, _, _)| n.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
GetBucketInfoKey {
|
||||
access_key_id: key.key_id,
|
||||
name: p.name.get().to_string(),
|
||||
name: st.name.get().to_string(),
|
||||
permissions,
|
||||
bucket_local_aliases: p
|
||||
.local_aliases
|
||||
.items()
|
||||
.iter()
|
||||
.filter(|(_, _, b)| *b == Some(bucket.id))
|
||||
.map(|(n, _, _)| n.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
})
|
||||
bucket_local_aliases,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
objects: *counters.get(OBJECTS).unwrap_or(&0),
|
||||
|
||||
+32
-26
@@ -8,6 +8,7 @@ use garage_util::time::now_msec;
|
||||
|
||||
use garage_model::garage::Garage;
|
||||
use garage_model::key_table::*;
|
||||
use garage_model::permission::ExpirationTime;
|
||||
|
||||
use crate::api::*;
|
||||
use crate::error::*;
|
||||
@@ -40,8 +41,8 @@ impl RequestHandler for ListKeysRequest {
|
||||
DateTime::from_timestamp_millis(x as i64)
|
||||
.expect("invalid timestamp stored in db")
|
||||
}),
|
||||
expiration: p.expiration.get().map(|x| {
|
||||
DateTime::from_timestamp_millis(x as i64)
|
||||
expiration: p.expiration.get().inner().map(|x| {
|
||||
DateTime::from_timestamp_millis(x.0 as i64)
|
||||
.expect("invalid timestamp stored in db")
|
||||
}),
|
||||
expired: p.is_expired(now),
|
||||
@@ -187,38 +188,42 @@ async fn key_info_results(
|
||||
key: Key,
|
||||
show_secret: bool,
|
||||
) -> Result<GetKeyInfoResponse, Error> {
|
||||
let mut relevant_buckets = HashMap::new();
|
||||
|
||||
let key_state = key.state.as_option().unwrap();
|
||||
|
||||
for id in key_state
|
||||
let buckets1 = key_state
|
||||
.authorized_buckets
|
||||
.items()
|
||||
.iter()
|
||||
.map(|(id, _)| id)
|
||||
.chain(
|
||||
key_state
|
||||
.local_aliases
|
||||
.items()
|
||||
.iter()
|
||||
.filter_map(|(_, _, v)| v.as_ref()),
|
||||
) {
|
||||
if !relevant_buckets.contains_key(id) {
|
||||
if let Some(b) = garage.bucket_table.get(&EmptyKey, id).await? {
|
||||
if b.state.as_option().is_some() {
|
||||
relevant_buckets.insert(*id, b);
|
||||
}
|
||||
.filter(|(_, p)| p.is_any())
|
||||
.map(|(id, _)| id);
|
||||
let buckets2 = key_state
|
||||
.local_aliases
|
||||
.items()
|
||||
.iter()
|
||||
.filter_map(|(_, _, v)| v.inner());
|
||||
|
||||
let mut relevant_buckets = HashMap::new();
|
||||
for bucket_id in buckets1.chain(buckets2) {
|
||||
if !relevant_buckets.contains_key(bucket_id) {
|
||||
if let Some(b) = garage.bucket_table.get(&EmptyKey, bucket_id).await? {
|
||||
relevant_buckets.insert(*bucket_id, b);
|
||||
} else {
|
||||
warn!(
|
||||
"Key {} references non-existent bucket {:?}",
|
||||
key.key_id, bucket_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
relevant_buckets.retain(|_, b| !b.is_deleted());
|
||||
|
||||
let res = GetKeyInfoResponse {
|
||||
name: key_state.name.get().clone(),
|
||||
created: key_state.created.map(|x| {
|
||||
DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db")
|
||||
}),
|
||||
expiration: key_state.expiration.get().map(|x| {
|
||||
DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db")
|
||||
expiration: key_state.expiration.get().inner().map(|x| {
|
||||
DateTime::from_timestamp_millis(x.0 as i64).expect("invalid timestamp stored in db")
|
||||
}),
|
||||
expired: key_state.is_expired(now_msec()),
|
||||
access_key_id: key.key_id.clone(),
|
||||
@@ -232,7 +237,7 @@ async fn key_info_results(
|
||||
},
|
||||
buckets: relevant_buckets
|
||||
.into_values()
|
||||
.filter_map(|bucket| {
|
||||
.map(|bucket| {
|
||||
let state = bucket.state.as_option().unwrap();
|
||||
let permissions = key_state
|
||||
.authorized_buckets
|
||||
@@ -242,8 +247,9 @@ async fn key_info_results(
|
||||
read: p.allow_read,
|
||||
write: p.allow_write,
|
||||
owner: p.allow_owner,
|
||||
})?;
|
||||
Some(KeyInfoBucketResponse {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
KeyInfoBucketResponse {
|
||||
id: hex::encode(bucket.id),
|
||||
global_aliases: state
|
||||
.aliases
|
||||
@@ -260,7 +266,7 @@ async fn key_info_results(
|
||||
.map(|((_, n), _, _)| n.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
permissions,
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
};
|
||||
@@ -283,10 +289,10 @@ fn apply_key_updates(key: &mut Key, updates: UpdateKeyRequestBody) -> Result<(),
|
||||
if let Some(expiration) = updates.expiration {
|
||||
key_state
|
||||
.expiration
|
||||
.update(Some(expiration.timestamp_millis() as u64));
|
||||
.update(Some(ExpirationTime(expiration.timestamp_millis() as u64)).into());
|
||||
}
|
||||
if updates.never_expires {
|
||||
key_state.expiration.update(None);
|
||||
key_state.expiration.update(None.into());
|
||||
}
|
||||
if let Some(allow) = updates.allow {
|
||||
if allow.create_bucket {
|
||||
|
||||
@@ -360,14 +360,7 @@ impl Worker for BlockRcRepair {
|
||||
_must_exit: &mut watch::Receiver<bool>,
|
||||
) -> Result<WorkerState, GarageError> {
|
||||
for _i in 0..RC_REPAIR_ITER_COUNT {
|
||||
let next1 = self
|
||||
.block_manager
|
||||
.rc
|
||||
.rc_table
|
||||
.range(self.cursor.as_slice()..)?
|
||||
.next()
|
||||
.transpose()?
|
||||
.map(|(k, _)| Hash::try_from(k.as_slice()).unwrap());
|
||||
let next1 = self.block_manager.rc.get_first_hash_from(self.cursor)?;
|
||||
let next2 = self
|
||||
.block_ref_table
|
||||
.data
|
||||
|
||||
@@ -164,7 +164,7 @@ async fn check_domain(garage: &Arc<Garage>, domain: &str) -> Result<bool, Error>
|
||||
}
|
||||
|
||||
let bucket_state = bucket.state.as_option().unwrap();
|
||||
let bucket_website_config = bucket_state.website_config.get();
|
||||
let bucket_website_config = bucket_state.website_config.get().inner();
|
||||
|
||||
match bucket_website_config {
|
||||
Some(_v) => Ok(true),
|
||||
|
||||
+13
-10
@@ -19,7 +19,7 @@ pub fn find_matching_cors_rule<'a, B>(
|
||||
bucket_params: &'a BucketParams,
|
||||
req: &'a Request<B>,
|
||||
) -> Result<Option<(&'a GarageCorsRule, &'a str)>, CommonError> {
|
||||
if let Some(cors_config) = bucket_params.cors_config.get() {
|
||||
if let Some(cors_config) = bucket_params.cors_config.get().inner() {
|
||||
if let Some(origin) = req.headers().get("Origin") {
|
||||
let origin = origin.to_str()?;
|
||||
let request_headers = match req.headers().get(ACCESS_CONTROL_REQUEST_HEADERS) {
|
||||
@@ -158,7 +158,7 @@ pub fn handle_options_for_bucket<B>(
|
||||
None => vec![],
|
||||
};
|
||||
|
||||
if let Some(cors_config) = bucket_params.cors_config.get() {
|
||||
if let Some(cors_config) = bucket_params.cors_config.get().inner() {
|
||||
let matching_rule = cors_config
|
||||
.iter()
|
||||
.find(|rule| cors_rule_matches(rule, origin, request_method, request_headers.iter()));
|
||||
@@ -192,14 +192,17 @@ mod tests {
|
||||
|
||||
fn bucket_params_with_rule(allow_origins: Vec<&str>) -> BucketParams {
|
||||
let mut bucket_params = BucketParams::default();
|
||||
bucket_params.cors_config.update(Some(vec![GarageCorsRule {
|
||||
id: Some("cors-test".into()),
|
||||
max_age_seconds: None,
|
||||
allow_origins: allow_origins.into_iter().map(str::to_string).collect(),
|
||||
allow_methods: vec!["GET".into(), "PUT".into()],
|
||||
allow_headers: vec!["*".into()],
|
||||
expose_headers: vec![],
|
||||
}]));
|
||||
bucket_params.cors_config.update(
|
||||
Some(vec![GarageCorsRule {
|
||||
id: Some("cors-test".into()),
|
||||
max_age_seconds: None,
|
||||
allow_origins: allow_origins.into_iter().map(str::to_string).collect(),
|
||||
allow_methods: vec!["GET".into(), "PUT".into()],
|
||||
allow_headers: vec!["*".into()],
|
||||
expose_headers: vec![],
|
||||
}])
|
||||
.into(),
|
||||
);
|
||||
bucket_params
|
||||
}
|
||||
|
||||
|
||||
+16
-8
@@ -5,12 +5,13 @@ use serde::{Deserialize, Serialize};
|
||||
use garage_table::{EnumerationOrder, TableSchema};
|
||||
|
||||
use garage_model::k2v::item_table::*;
|
||||
use garage_model::k2v::rpc::K2VMonotonicRead;
|
||||
|
||||
use garage_api_common::helpers::*;
|
||||
|
||||
use crate::api_server::{ReqBody, ResBody};
|
||||
use crate::error::*;
|
||||
use crate::item::parse_causality_token;
|
||||
use crate::item::{is_monotonic_read, parse_causality_token};
|
||||
use crate::range::read_range;
|
||||
|
||||
pub async fn handle_insert_batch(
|
||||
@@ -47,12 +48,13 @@ pub async fn handle_read_batch(
|
||||
ctx: ReqCtx,
|
||||
req: Request<ReqBody>,
|
||||
) -> Result<Response<ResBody>, Error> {
|
||||
let monotonic_read = is_monotonic_read(&req)?;
|
||||
let queries = req.into_body().json::<Vec<ReadBatchQuery>>().await?;
|
||||
|
||||
let resp_results = futures::future::join_all(
|
||||
queries
|
||||
.into_iter()
|
||||
.map(|q| handle_read_batch_query(&ctx, q)),
|
||||
.map(|q| handle_read_batch_query(&ctx, q, monotonic_read)),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -67,6 +69,7 @@ pub async fn handle_read_batch(
|
||||
async fn handle_read_batch_query(
|
||||
ctx: &ReqCtx,
|
||||
query: ReadBatchQuery,
|
||||
monotonic_read: K2VMonotonicRead,
|
||||
) -> Result<ReadBatchResponse, Error> {
|
||||
let ReqCtx {
|
||||
garage, bucket_id, ..
|
||||
@@ -90,12 +93,13 @@ async fn handle_read_batch_query(
|
||||
.start
|
||||
.as_ref()
|
||||
.ok_or_bad_request("start should be specified if single_item is set")?;
|
||||
let item = garage
|
||||
.k2v
|
||||
.item_table
|
||||
.get(&partition, sk)
|
||||
.await?
|
||||
.filter(|e| K2VItemTable::matches_filter(e, &filter));
|
||||
let item = match monotonic_read {
|
||||
K2VMonotonicRead::Monotonic => {
|
||||
garage.k2v.item_table.get_monotonic(&partition, sk).await?
|
||||
}
|
||||
K2VMonotonicRead::NonMonotonic => garage.k2v.item_table.get(&partition, sk).await?,
|
||||
}
|
||||
.filter(|e| K2VItemTable::matches_filter(e, &filter));
|
||||
match item {
|
||||
Some(i) => (vec![ReadBatchResponseItem::from(i)], false, None),
|
||||
None => (vec![], false, None),
|
||||
@@ -110,6 +114,7 @@ async fn handle_read_batch_query(
|
||||
query.limit,
|
||||
Some(filter),
|
||||
EnumerationOrder::from_reverse(query.reverse),
|
||||
monotonic_read,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -218,6 +223,7 @@ async fn handle_delete_batch_query(
|
||||
None,
|
||||
Some(filter),
|
||||
EnumerationOrder::Forward,
|
||||
K2VMonotonicRead::NonMonotonic,
|
||||
)
|
||||
.await?;
|
||||
assert!(!more);
|
||||
@@ -260,6 +266,7 @@ pub(crate) async fn handle_poll_range(
|
||||
let ReqCtx {
|
||||
garage, bucket_id, ..
|
||||
} = ctx;
|
||||
let monotonic_read = is_monotonic_read(&req)?;
|
||||
use garage_model::k2v::sub::PollRange;
|
||||
|
||||
let query = req.into_body().json::<PollRangeQuery>().await?;
|
||||
@@ -281,6 +288,7 @@ pub(crate) async fn handle_poll_range(
|
||||
},
|
||||
query.seen_marker,
|
||||
timeout_msec,
|
||||
monotonic_read,
|
||||
)
|
||||
.await
|
||||
.map_err(pass_helper_error)?;
|
||||
|
||||
@@ -44,6 +44,10 @@ pub enum Error {
|
||||
#[error("Invalid causality token")]
|
||||
InvalidCausalityToken,
|
||||
|
||||
/// Invalid parameter for x-garage-non-monotonic-read
|
||||
#[error("Invalid X-Garage-Non-Monotonic-Read value: {0}")]
|
||||
InvalidNonMonotonicRead(String),
|
||||
|
||||
/// The client asked for an invalid return format (invalid Accept header)
|
||||
#[error("Not acceptable: {0}")]
|
||||
NotAcceptable(String),
|
||||
@@ -85,6 +89,7 @@ impl Error {
|
||||
Error::InvalidBase64(_) => "InvalidBase64",
|
||||
Error::InvalidUtf8Str(_) => "InvalidUtf8String",
|
||||
Error::InvalidCausalityToken => "CausalityToken",
|
||||
Error::InvalidNonMonotonicRead(_) => "InvalidNonMonotonicRead",
|
||||
Error::InvalidDigest(_) => "InvalidDigest",
|
||||
}
|
||||
}
|
||||
@@ -101,7 +106,8 @@ impl ApiError for Error {
|
||||
| Error::InvalidBase64(_)
|
||||
| Error::InvalidUtf8Str(_)
|
||||
| Error::InvalidDigest(_)
|
||||
| Error::InvalidCausalityToken => StatusCode::BAD_REQUEST,
|
||||
| Error::InvalidCausalityToken
|
||||
| Error::InvalidNonMonotonicRead(_) => StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use serde::Serialize;
|
||||
use garage_table::util::*;
|
||||
|
||||
use garage_model::k2v::item_table::{BYTES, CONFLICTS, ENTRIES, VALUES};
|
||||
use garage_model::k2v::rpc::K2VMonotonicRead;
|
||||
|
||||
use garage_api_common::helpers::*;
|
||||
|
||||
@@ -40,6 +41,7 @@ pub async fn handle_read_index(
|
||||
limit,
|
||||
Some((DeletedFilter::NotDeleted, node_id_vec)),
|
||||
EnumerationOrder::from_reverse(reverse),
|
||||
K2VMonotonicRead::NonMonotonic,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
+37
-13
@@ -5,6 +5,7 @@ use hyper::{Request, Response, StatusCode};
|
||||
|
||||
use garage_model::k2v::causality::*;
|
||||
use garage_model::k2v::item_table::*;
|
||||
use garage_model::k2v::rpc::K2VMonotonicRead;
|
||||
|
||||
use garage_api_common::helpers::*;
|
||||
|
||||
@@ -12,6 +13,7 @@ use crate::api_server::{ReqBody, ResBody};
|
||||
use crate::error::*;
|
||||
|
||||
pub const X_GARAGE_CAUSALITY_TOKEN: &str = "X-Garage-Causality-Token";
|
||||
pub const X_GARAGE_NON_MONOTONIC_READ: &str = "X-Garage-Non-Monotonic-Read";
|
||||
|
||||
pub enum ReturnFormat {
|
||||
Json,
|
||||
@@ -23,6 +25,21 @@ pub(crate) fn parse_causality_token(s: &str) -> Result<CausalContext, Error> {
|
||||
CausalContext::parse(s).ok_or(Error::InvalidCausalityToken)
|
||||
}
|
||||
|
||||
pub(crate) fn is_monotonic_read(req: &Request<ReqBody>) -> Result<K2VMonotonicRead, Error> {
|
||||
let v_opt = req
|
||||
.headers()
|
||||
.get(X_GARAGE_NON_MONOTONIC_READ)
|
||||
.map(|s| s.to_str())
|
||||
.transpose()?;
|
||||
|
||||
match v_opt {
|
||||
Some("true") => Ok(K2VMonotonicRead::NonMonotonic),
|
||||
// Reads are monotonic by default
|
||||
Some("false") | None => Ok(K2VMonotonicRead::Monotonic),
|
||||
Some(s) => Err(Error::InvalidNonMonotonicRead(s.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
impl ReturnFormat {
|
||||
pub fn from(req: &Request<ReqBody>) -> Result<Self, Error> {
|
||||
let accept = match req.headers().get(header::ACCEPT) {
|
||||
@@ -108,21 +125,26 @@ pub async fn handle_read_item(
|
||||
let ReqCtx {
|
||||
garage, bucket_id, ..
|
||||
} = &ctx;
|
||||
|
||||
let monotonic_read = is_monotonic_read(req)?;
|
||||
let format = ReturnFormat::from(req)?;
|
||||
let partition_key = K2VItemPartition {
|
||||
bucket_id: *bucket_id,
|
||||
partition_key: partition_key.to_string(),
|
||||
};
|
||||
|
||||
let item = garage
|
||||
.k2v
|
||||
.item_table
|
||||
.get(
|
||||
&K2VItemPartition {
|
||||
bucket_id: *bucket_id,
|
||||
partition_key: partition_key.to_string(),
|
||||
},
|
||||
sort_key,
|
||||
)
|
||||
.await?
|
||||
.ok_or(Error::NoSuchKey)?;
|
||||
let item = match monotonic_read {
|
||||
K2VMonotonicRead::Monotonic => {
|
||||
garage
|
||||
.k2v
|
||||
.item_table
|
||||
.get_monotonic(&partition_key, sort_key)
|
||||
.await?
|
||||
}
|
||||
K2VMonotonicRead::NonMonotonic => {
|
||||
garage.k2v.item_table.get(&partition_key, sort_key).await?
|
||||
}
|
||||
}
|
||||
.ok_or(Error::NoSuchKey)?;
|
||||
|
||||
format.make_response(&item)
|
||||
}
|
||||
@@ -214,6 +236,7 @@ pub async fn handle_poll_item(
|
||||
let ReqCtx {
|
||||
garage, bucket_id, ..
|
||||
} = &ctx;
|
||||
let monotonic_read = is_monotonic_read(req)?;
|
||||
let format = ReturnFormat::from(req)?;
|
||||
|
||||
let causal_context =
|
||||
@@ -230,6 +253,7 @@ pub async fn handle_poll_item(
|
||||
sort_key,
|
||||
causal_context,
|
||||
timeout_msec,
|
||||
monotonic_read,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
+27
-9
@@ -4,6 +4,8 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use garage_model::k2v::rpc::K2VMonotonicRead;
|
||||
|
||||
use garage_table::replication::TableShardedReplication;
|
||||
use garage_table::*;
|
||||
|
||||
@@ -23,6 +25,7 @@ pub(crate) async fn read_range<F>(
|
||||
limit: Option<u64>,
|
||||
filter: Option<F::Filter>,
|
||||
enumeration_order: EnumerationOrder,
|
||||
monotonic_read: K2VMonotonicRead,
|
||||
) -> Result<(Vec<F::E>, bool, Option<String>), Error>
|
||||
where
|
||||
F: TableSchema<S = String> + 'static,
|
||||
@@ -53,15 +56,30 @@ where
|
||||
1000,
|
||||
limit.map(|x| x as usize).unwrap_or(usize::MAX - 10) - entries.len() + 2,
|
||||
);
|
||||
let get_ret = table
|
||||
.get_range(
|
||||
partition_key,
|
||||
start.clone(),
|
||||
filter.clone(),
|
||||
n_get,
|
||||
enumeration_order,
|
||||
)
|
||||
.await?;
|
||||
let get_ret = match monotonic_read {
|
||||
K2VMonotonicRead::Monotonic => {
|
||||
table
|
||||
.get_range_monotonic(
|
||||
partition_key,
|
||||
start.clone(),
|
||||
filter.clone(),
|
||||
n_get,
|
||||
enumeration_order,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
K2VMonotonicRead::NonMonotonic => {
|
||||
table
|
||||
.get_range(
|
||||
partition_key,
|
||||
start.clone(),
|
||||
filter.clone(),
|
||||
n_get,
|
||||
enumeration_order,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
let get_ret_len = get_ret.len();
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ pub async fn handle_list_buckets(
|
||||
for (alias, _, _active) in bucket.aliases().iter().filter(|(_, _, active)| *active) {
|
||||
let alias_opt = garage.bucket_alias_table.get(&EmptyKey, alias).await?;
|
||||
if let Some(alias_ent) = alias_opt {
|
||||
if *alias_ent.state.get() == Some(*bucket_id) {
|
||||
if alias_ent.state.get().inner() == Some(bucket_id) {
|
||||
aliases.insert(alias_ent.name().to_string(), *bucket_id);
|
||||
}
|
||||
}
|
||||
@@ -134,7 +134,7 @@ pub async fn handle_list_buckets(
|
||||
}
|
||||
|
||||
for (alias, _, id_opt) in key_p.local_aliases.items() {
|
||||
if let Some(id) = id_opt {
|
||||
if let Some(id) = id_opt.inner() {
|
||||
aliases.insert(alias.clone(), *id);
|
||||
}
|
||||
}
|
||||
@@ -256,7 +256,10 @@ pub async fn handle_delete_bucket(ctx: ReqCtx) -> Result<Response<ResBody>, Erro
|
||||
|
||||
let key_params = api_key.params().unwrap();
|
||||
|
||||
let is_local_alias = matches!(key_params.local_aliases.get(bucket_name), Some(Some(_)));
|
||||
let is_local_alias = matches!(
|
||||
key_params.local_aliases.get(bucket_name).map(|x| x.inner()),
|
||||
Some(Some(_))
|
||||
);
|
||||
|
||||
// If the bucket has no other aliases, this is a true deletion.
|
||||
// Otherwise, it is just an alias removal.
|
||||
|
||||
+3
-3
@@ -13,7 +13,7 @@ use crate::xml::to_xml_with_header;
|
||||
|
||||
pub async fn handle_get_cors(ctx: ReqCtx) -> Result<Response<ResBody>, Error> {
|
||||
let ReqCtx { bucket_params, .. } = ctx;
|
||||
if let Some(cors) = bucket_params.cors_config.get() {
|
||||
if let Some(cors) = bucket_params.cors_config.get().inner() {
|
||||
let wc = CorsConfiguration {
|
||||
xmlns: (),
|
||||
cors_rules: cors
|
||||
@@ -38,7 +38,7 @@ pub async fn handle_delete_cors(ctx: ReqCtx) -> Result<Response<ResBody>, Error>
|
||||
mut bucket_params,
|
||||
..
|
||||
} = ctx;
|
||||
bucket_params.cors_config.update(None);
|
||||
bucket_params.cors_config.update(None.into());
|
||||
garage
|
||||
.bucket_table
|
||||
.insert(&Bucket::present(bucket_id, bucket_params))
|
||||
@@ -67,7 +67,7 @@ pub async fn handle_put_cors(
|
||||
|
||||
bucket_params
|
||||
.cors_config
|
||||
.update(Some(conf.into_garage_cors_config()?));
|
||||
.update(Some(conf.into_garage_cors_config()?).into());
|
||||
garage
|
||||
.bucket_table
|
||||
.insert(&Bucket::present(bucket_id, bucket_params))
|
||||
|
||||
+15
-2
@@ -83,8 +83,21 @@ pub async fn handle_delete_objects(
|
||||
}
|
||||
ret_deleted.push(s3_xml::Deleted {
|
||||
key: s3_xml::Value(obj.key.clone()),
|
||||
version_id: s3_xml::Value(hex::encode(deleted_version)),
|
||||
delete_marker_version_id: s3_xml::Value(hex::encode(delete_marker_version)),
|
||||
version_id: Some(s3_xml::Value(hex::encode(deleted_version))),
|
||||
delete_marker_version_id: Some(s3_xml::Value(hex::encode(
|
||||
delete_marker_version,
|
||||
))),
|
||||
});
|
||||
}
|
||||
Err(Error::NoSuchKey) => {
|
||||
if cmd.quiet {
|
||||
continue;
|
||||
}
|
||||
// Deleting a non-existent key is a success in S3
|
||||
ret_deleted.push(s3_xml::Deleted {
|
||||
key: s3_xml::Value(obj.key.clone()),
|
||||
version_id: None,
|
||||
delete_marker_version_id: None,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
@@ -14,7 +14,7 @@ use garage_model::bucket_table::Bucket;
|
||||
pub async fn handle_get_lifecycle(ctx: ReqCtx) -> Result<Response<ResBody>, Error> {
|
||||
let ReqCtx { bucket_params, .. } = ctx;
|
||||
|
||||
if let Some(lifecycle) = bucket_params.lifecycle_config.get() {
|
||||
if let Some(lifecycle) = bucket_params.lifecycle_config.get().inner() {
|
||||
let wc = LifecycleConfiguration::from_garage_lifecycle_config(lifecycle);
|
||||
let xml = to_xml_with_header(&wc)?;
|
||||
Ok(Response::builder()
|
||||
@@ -33,7 +33,7 @@ pub async fn handle_delete_lifecycle(ctx: ReqCtx) -> Result<Response<ResBody>, E
|
||||
mut bucket_params,
|
||||
..
|
||||
} = ctx;
|
||||
bucket_params.lifecycle_config.update(None);
|
||||
bucket_params.lifecycle_config.update(None.into());
|
||||
garage
|
||||
.bucket_table
|
||||
.insert(&Bucket::present(bucket_id, bucket_params))
|
||||
@@ -62,7 +62,7 @@ pub async fn handle_put_lifecycle(
|
||||
.validate_into_garage_lifecycle_config()
|
||||
.ok_or_bad_request("Invalid lifecycle configuration")?;
|
||||
|
||||
bucket_params.lifecycle_config.update(Some(config));
|
||||
bucket_params.lifecycle_config.update(Some(config).into());
|
||||
garage
|
||||
.bucket_table
|
||||
.insert(&Bucket::present(bucket_id, bucket_params))
|
||||
|
||||
@@ -83,10 +83,12 @@ pub async fn handle_post_object(
|
||||
};
|
||||
|
||||
// Current part is file. Do some checks before handling to PutObject code
|
||||
let key = params
|
||||
.get("key")
|
||||
.ok_or_bad_request("No key was provided")?
|
||||
.to_str()?;
|
||||
let key = std::str::from_utf8(
|
||||
params
|
||||
.get("key")
|
||||
.ok_or_bad_request("No key was provided")?
|
||||
.as_bytes(),
|
||||
)?;
|
||||
let policy = params
|
||||
.get("policy")
|
||||
.ok_or_bad_request("No policy was provided")?
|
||||
|
||||
+4
-1
@@ -679,7 +679,10 @@ pub(crate) fn extract_metadata_headers(
|
||||
];
|
||||
for name in standard_header.iter() {
|
||||
if let Some(value) = headers.get(name) {
|
||||
ret.push((name.to_string(), value.to_str()?.to_string()));
|
||||
ret.push((
|
||||
name.to_string(),
|
||||
std::str::from_utf8(value.as_bytes())?.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ pub const X_AMZ_WEBSITE_REDIRECT_LOCATION: HeaderName =
|
||||
|
||||
pub async fn handle_get_website(ctx: ReqCtx) -> Result<Response<ResBody>, Error> {
|
||||
let ReqCtx { bucket_params, .. } = ctx;
|
||||
if let Some(website) = bucket_params.website_config.get() {
|
||||
if let Some(website) = bucket_params.website_config.get().inner() {
|
||||
let wc = WebsiteConfiguration {
|
||||
xmlns: (),
|
||||
error_document: website.error_document.as_ref().map(|v| Key {
|
||||
@@ -54,7 +54,7 @@ pub async fn handle_delete_website(ctx: ReqCtx) -> Result<Response<ResBody>, Err
|
||||
mut bucket_params,
|
||||
..
|
||||
} = ctx;
|
||||
bucket_params.website_config.update(None);
|
||||
bucket_params.website_config.update(None.into());
|
||||
garage
|
||||
.bucket_table
|
||||
.insert(&Bucket::present(bucket_id, bucket_params))
|
||||
@@ -83,7 +83,7 @@ pub async fn handle_put_website(
|
||||
|
||||
bucket_params
|
||||
.website_config
|
||||
.update(Some(conf.into_garage_website_config()?));
|
||||
.update(Some(conf.into_garage_website_config()?).into());
|
||||
garage
|
||||
.bucket_table
|
||||
.insert(&Bucket::present(bucket_id, bucket_params))
|
||||
|
||||
+11
-8
@@ -44,10 +44,13 @@ pub struct LocationConstraint {
|
||||
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,
|
||||
#[serde(rename = "VersionId", skip_serializing_if = "Option::is_none")]
|
||||
pub version_id: Option<Value>,
|
||||
#[serde(
|
||||
rename = "DeleteMarkerVersionId",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub delete_marker_version_id: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
@@ -497,13 +500,13 @@ mod tests {
|
||||
deleted: vec![
|
||||
Deleted {
|
||||
key: Value("a/plop".to_string()),
|
||||
version_id: Value("qsdfjklm".to_string()),
|
||||
delete_marker_version_id: Value("wxcvbn".to_string()),
|
||||
version_id: Some(Value("qsdfjklm".to_string())),
|
||||
delete_marker_version_id: Some(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()),
|
||||
version_id: Some(Value("1234".to_string())),
|
||||
delete_marker_version_id: Some(Value("4321".to_string())),
|
||||
},
|
||||
],
|
||||
errors: vec![
|
||||
|
||||
@@ -144,7 +144,7 @@ impl BlockManager {
|
||||
|
||||
// Open metadata tables
|
||||
let rc = db
|
||||
.open_tree("block_local_rc")
|
||||
.open_typed_tree("block_local_rc")
|
||||
.expect("Unable to open block_local_rc tree");
|
||||
let rc = BlockRc::new(rc);
|
||||
|
||||
@@ -158,9 +158,9 @@ impl BlockManager {
|
||||
|
||||
let metrics = BlockManagerMetrics::new(
|
||||
config.compression_level,
|
||||
rc.rc_table.clone(),
|
||||
resync.queue.clone(),
|
||||
resync.errors.clone(),
|
||||
rc.rc_table.untyped().clone(),
|
||||
resync.queue.untyped().clone(),
|
||||
resync.errors.untyped().clone(),
|
||||
buffer_kb_semaphore.clone(),
|
||||
);
|
||||
|
||||
@@ -449,9 +449,8 @@ impl BlockManager {
|
||||
let mut blocks = Vec::with_capacity(self.resync.errors.approximate_len()?);
|
||||
for ent in self.resync.errors.iter()? {
|
||||
let (hash, cnt) = ent?;
|
||||
let cnt = ErrorCounter::decode(&cnt);
|
||||
blocks.push(BlockResyncErrorInfo {
|
||||
hash: Hash::try_from(&hash).unwrap(),
|
||||
hash,
|
||||
refcount: 0,
|
||||
error_count: cnt.errors,
|
||||
last_try: cnt.last_try,
|
||||
|
||||
+103
-98
@@ -1,4 +1,5 @@
|
||||
use std::convert::TryInto;
|
||||
use std::num::NonZeroU64;
|
||||
|
||||
use arc_swap::ArcSwapOption;
|
||||
|
||||
@@ -14,12 +15,12 @@ pub type CalculateRefcount =
|
||||
Box<dyn Fn(&db::Transaction, &Hash) -> db::TxResult<usize, Error> + Send + Sync>;
|
||||
|
||||
pub struct BlockRc {
|
||||
pub rc_table: db::Tree,
|
||||
pub(crate) rc_table: db::TypedTree<Hash, RcEntry>,
|
||||
pub(crate) recalc_rc: ArcSwapOption<Vec<CalculateRefcount>>,
|
||||
}
|
||||
|
||||
impl BlockRc {
|
||||
pub(crate) fn new(rc: db::Tree) -> Self {
|
||||
pub(crate) fn new(rc: db::TypedTree<Hash, RcEntry>) -> Self {
|
||||
Self {
|
||||
rc_table: rc,
|
||||
recalc_rc: ArcSwapOption::new(None),
|
||||
@@ -33,11 +34,8 @@ impl BlockRc {
|
||||
tx: &mut db::Transaction,
|
||||
hash: &Hash,
|
||||
) -> db::TxOpResult<bool> {
|
||||
let old_rc = RcEntry::parse_opt(tx.get(&self.rc_table, hash)?);
|
||||
match old_rc.increment().serialize() {
|
||||
Some(x) => tx.insert(&self.rc_table, hash, x)?,
|
||||
None => unreachable!(),
|
||||
}
|
||||
let old_rc = RcState(self.rc_table.tx_get(tx, hash)?);
|
||||
self.rc_table.tx_insert(tx, hash, &old_rc.increment())?;
|
||||
Ok(old_rc.is_zero())
|
||||
}
|
||||
|
||||
@@ -48,17 +46,27 @@ impl BlockRc {
|
||||
tx: &mut db::Transaction,
|
||||
hash: &Hash,
|
||||
) -> db::TxOpResult<bool> {
|
||||
let new_rc = RcEntry::parse_opt(tx.get(&self.rc_table, hash)?).decrement();
|
||||
match new_rc.serialize() {
|
||||
Some(x) => tx.insert(&self.rc_table, hash, x)?,
|
||||
None => tx.remove(&self.rc_table, hash)?,
|
||||
let new_rc = RcState(self.rc_table.tx_get(tx, hash)?).decrement();
|
||||
match &new_rc.0 {
|
||||
None => self.rc_table.tx_remove(tx, hash)?,
|
||||
Some(rc) => self.rc_table.tx_insert(tx, hash, rc)?,
|
||||
}
|
||||
Ok(matches!(new_rc, RcEntry::Deletable { .. }))
|
||||
Ok(matches!(new_rc.0, Some(RcEntry::Deletable { .. })))
|
||||
}
|
||||
|
||||
/// Read a block's reference count
|
||||
pub(crate) fn get_block_rc(&self, hash: &Hash) -> Result<RcEntry, Error> {
|
||||
Ok(RcEntry::parse_opt(self.rc_table.get(hash.as_ref())?))
|
||||
/// Read a block's reference counting state
|
||||
pub(crate) fn get_block_rc(&self, hash: &Hash) -> Result<RcState, Error> {
|
||||
Ok(RcState(self.rc_table.get(hash)?))
|
||||
}
|
||||
|
||||
/// Return the first hash stored in the RC table at or after `cursor`
|
||||
pub fn get_first_hash_from(&self, cursor: Hash) -> Result<Option<Hash>, Error> {
|
||||
Ok(self
|
||||
.rc_table
|
||||
.range(cursor..)?
|
||||
.next()
|
||||
.transpose()?
|
||||
.map(|(k, _)| k))
|
||||
}
|
||||
|
||||
/// Delete an entry in the RC table if it is deletable and the
|
||||
@@ -66,12 +74,11 @@ impl BlockRc {
|
||||
pub(crate) fn clear_deleted_block_rc(&self, hash: &Hash) -> Result<(), Error> {
|
||||
let now = now_msec();
|
||||
self.rc_table.db().transaction(|tx| {
|
||||
let rcval = RcEntry::parse_opt(tx.get(&self.rc_table, hash)?);
|
||||
match rcval {
|
||||
RcEntry::Deletable { at_time } if now > at_time => {
|
||||
tx.remove(&self.rc_table, hash)?;
|
||||
let rcval = self.rc_table.tx_get(tx, hash)?;
|
||||
if let Some(RcEntry::Deletable { at_time }) = rcval {
|
||||
if now > at_time {
|
||||
self.rc_table.tx_remove(tx, hash)?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
@@ -91,28 +98,25 @@ impl BlockRc {
|
||||
for f in recalc_fns.iter() {
|
||||
cnt += f(tx, hash)?;
|
||||
}
|
||||
let old_rc = RcEntry::parse_opt(tx.get(&self.rc_table, hash)?);
|
||||
let old_count = RcState(self.rc_table.tx_get(tx, hash)?).as_u64();
|
||||
trace!(
|
||||
"Block RC for {:?}: stored={}, calculated={}",
|
||||
hash,
|
||||
old_rc.as_u64(),
|
||||
old_count,
|
||||
cnt
|
||||
);
|
||||
if cnt as u64 != old_rc.as_u64() {
|
||||
if cnt as u64 != old_count {
|
||||
warn!(
|
||||
"Fixing inconsistent block RC for {:?}: was {}, should be {}",
|
||||
hash,
|
||||
old_rc.as_u64(),
|
||||
cnt
|
||||
hash, old_count, cnt
|
||||
);
|
||||
let new_rc = if cnt > 0 {
|
||||
RcEntry::Present { count: cnt as u64 }
|
||||
} else {
|
||||
RcEntry::Deletable {
|
||||
let new_rc = match NonZeroU64::new(cnt as u64) {
|
||||
Some(count) => RcEntry::Present { count },
|
||||
None => RcEntry::Deletable {
|
||||
at_time: now_msec() + BLOCK_GC_DELAY.as_millis() as u64,
|
||||
}
|
||||
},
|
||||
};
|
||||
tx.insert(&self.rc_table, hash, new_rc.serialize().unwrap())?;
|
||||
self.rc_table.tx_insert(tx, hash, &new_rc)?;
|
||||
Ok((cnt, true))
|
||||
} else {
|
||||
Ok((cnt, false))
|
||||
@@ -131,13 +135,47 @@ impl BlockRc {
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes the state of the reference counter for a block
|
||||
impl db::DbBytes for RcEntry {
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
match self {
|
||||
RcEntry::Present { count } => u64::to_be_bytes(count.get()).to_vec(),
|
||||
RcEntry::Deletable { at_time } => {
|
||||
[u64::to_be_bytes(0), u64::to_be_bytes(*at_time)].concat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> std::result::Result<Self, db::DecodeError> {
|
||||
if bytes.len() == 8 {
|
||||
let count = NonZeroU64::new(u64::from_be_bytes(bytes.try_into().unwrap()))
|
||||
.ok_or(db::DecodeError("invalid RC entry: zero count".into()))?;
|
||||
Ok(RcEntry::Present { count })
|
||||
} else if bytes.len() == 16 {
|
||||
Ok(RcEntry::Deletable {
|
||||
at_time: u64::from_be_bytes(bytes[8..16].try_into().unwrap()),
|
||||
})
|
||||
} else {
|
||||
Err(db::DecodeError(
|
||||
format!(
|
||||
"invalid RC entry: expected 8 or 16 bytes, got {}",
|
||||
bytes.len()
|
||||
)
|
||||
.into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A block's entry in the RC table.
|
||||
///
|
||||
/// A block with zero references and no pending deletion has no entry
|
||||
/// in the RC table at all: see [`RcState`].
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) enum RcEntry {
|
||||
/// Present: the block has `count` references, with `count` > 0.
|
||||
/// Present: the block has `count` references.
|
||||
///
|
||||
/// This is stored as `u64::to_be_bytes(count)`
|
||||
Present { count: u64 },
|
||||
Present { count: NonZeroU64 },
|
||||
|
||||
/// Deletable: the block has zero references, and can be deleted
|
||||
/// once time (returned by `now_msec`) is larger than `at_time`
|
||||
@@ -147,72 +185,39 @@ pub(crate) enum RcEntry {
|
||||
/// (this allows for the data format to be backwards compatible with
|
||||
/// previous Garage versions that didn't have this intermediate state)
|
||||
Deletable { at_time: u64 },
|
||||
|
||||
/// Absent: the block has zero references, and can be deleted
|
||||
/// immediately
|
||||
Absent,
|
||||
}
|
||||
|
||||
impl RcEntry {
|
||||
fn parse(bytes: &[u8]) -> Self {
|
||||
if bytes.len() == 8 {
|
||||
RcEntry::Present {
|
||||
count: u64::from_be_bytes(bytes.try_into().unwrap()),
|
||||
}
|
||||
} else if bytes.len() == 16 {
|
||||
RcEntry::Deletable {
|
||||
at_time: u64::from_be_bytes(bytes[8..16].try_into().unwrap()),
|
||||
}
|
||||
} else {
|
||||
panic!("Invalid RC entry: {:?}, database is corrupted. This is an error Garage is currently unable to recover from. Sorry, and also please report a bug.",
|
||||
bytes
|
||||
)
|
||||
}
|
||||
}
|
||||
/// Describes the state of the reference counter for a block: the block's
|
||||
/// entry in the RC table, or `None` if it has none, meaning the block has
|
||||
/// zero references and can be deleted immediately.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct RcState(Option<RcEntry>);
|
||||
|
||||
fn parse_opt<V: AsRef<[u8]>>(bytes: Option<V>) -> Self {
|
||||
bytes
|
||||
.map(|b| Self::parse(b.as_ref()))
|
||||
.unwrap_or(Self::Absent)
|
||||
}
|
||||
|
||||
fn serialize(self) -> Option<Vec<u8>> {
|
||||
match self {
|
||||
RcEntry::Present { count } => Some(u64::to_be_bytes(count).to_vec()),
|
||||
RcEntry::Deletable { at_time } => {
|
||||
Some([u64::to_be_bytes(0), u64::to_be_bytes(at_time)].concat())
|
||||
}
|
||||
RcEntry::Absent => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn increment(self) -> Self {
|
||||
let old_count = match self {
|
||||
RcEntry::Present { count } => count,
|
||||
_ => 0,
|
||||
impl RcState {
|
||||
/// The new RC table entry after a reference is taken on the block
|
||||
fn increment(&self) -> RcEntry {
|
||||
let count = match self.0 {
|
||||
Some(RcEntry::Present { count }) => count.saturating_add(1),
|
||||
_ => NonZeroU64::new(1).unwrap(),
|
||||
};
|
||||
RcEntry::Present {
|
||||
count: old_count + 1,
|
||||
}
|
||||
RcEntry::Present { count }
|
||||
}
|
||||
|
||||
fn decrement(self) -> Self {
|
||||
match self {
|
||||
RcEntry::Present { count } => {
|
||||
if count > 1 {
|
||||
RcEntry::Present { count: count - 1 }
|
||||
} else {
|
||||
RcEntry::Deletable {
|
||||
at_time: now_msec() + BLOCK_GC_DELAY.as_millis() as u64,
|
||||
}
|
||||
}
|
||||
}
|
||||
del => del,
|
||||
}
|
||||
/// The new state after a reference to the block is dropped
|
||||
fn decrement(&self) -> Self {
|
||||
RcState(match self.0 {
|
||||
Some(RcEntry::Present { count }) => Some(match NonZeroU64::new(count.get() - 1) {
|
||||
Some(count) => RcEntry::Present { count },
|
||||
None => RcEntry::Deletable {
|
||||
at_time: now_msec() + BLOCK_GC_DELAY.as_millis() as u64,
|
||||
},
|
||||
}),
|
||||
unchanged => unchanged,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn is_zero(&self) -> bool {
|
||||
matches!(self, RcEntry::Deletable { .. } | RcEntry::Absent)
|
||||
matches!(self.0, None | Some(RcEntry::Deletable { .. }))
|
||||
}
|
||||
|
||||
pub(crate) fn is_nonzero(&self) -> bool {
|
||||
@@ -220,10 +225,10 @@ impl RcEntry {
|
||||
}
|
||||
|
||||
pub(crate) fn is_deletable(&self) -> bool {
|
||||
match self {
|
||||
RcEntry::Present { .. } => false,
|
||||
RcEntry::Deletable { at_time } => now_msec() > *at_time,
|
||||
RcEntry::Absent => true,
|
||||
match self.0 {
|
||||
Some(RcEntry::Present { .. }) => false,
|
||||
Some(RcEntry::Deletable { at_time }) => now_msec() > at_time,
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,8 +237,8 @@ impl RcEntry {
|
||||
}
|
||||
|
||||
pub(crate) fn as_u64(&self) -> u64 {
|
||||
match self {
|
||||
RcEntry::Present { count } => *count,
|
||||
match self.0 {
|
||||
Some(RcEntry::Present { count }) => count.get(),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -102,16 +102,15 @@ impl Worker for RepairWorker {
|
||||
let mut batch_of_hashes = vec![];
|
||||
let start_bound = match self.next_start.as_ref() {
|
||||
None => Bound::Unbounded,
|
||||
Some(x) => Bound::Excluded(x.as_slice()),
|
||||
Some(x) => Bound::Excluded(*x),
|
||||
};
|
||||
for entry in self
|
||||
.manager
|
||||
.rc
|
||||
.rc_table
|
||||
.range::<&[u8], _>((start_bound, Bound::Unbounded))?
|
||||
.range((start_bound, Bound::Unbounded))?
|
||||
{
|
||||
let (hash, _) = entry?;
|
||||
let hash = Hash::try_from(&hash[..]).unwrap();
|
||||
batch_of_hashes.push(hash);
|
||||
if batch_of_hashes.len() >= 1000 {
|
||||
break;
|
||||
|
||||
+106
-45
@@ -44,15 +44,58 @@ pub(crate) const MAX_RESYNC_WORKERS: usize = 8;
|
||||
const INITIAL_RESYNC_TRANQUILITY: u32 = 2;
|
||||
|
||||
pub struct BlockResyncManager {
|
||||
pub(crate) queue: db::Tree,
|
||||
pub(crate) queue: db::TypedTree<ResyncQueueKey, Hash>,
|
||||
pub(crate) notify: Arc<Notify>,
|
||||
pub(crate) errors: db::Tree,
|
||||
pub(crate) errors: db::TypedTree<Hash, ErrorCounter>,
|
||||
|
||||
busy_set: BusySet,
|
||||
|
||||
persister: PersisterShared<ResyncPersistedConfig>,
|
||||
}
|
||||
|
||||
/// Key of the resync queue tree: blocks are resynced in order of increasing
|
||||
/// `when` (msec timestamp of the next try), with the block hash as tie-breaker.
|
||||
///
|
||||
// CAREFUL: this type implements `DbOrdKey`, so its byte encoding must be
|
||||
// order-preserving.
|
||||
// The derived `Ord` compares fields in declaration order, which must match
|
||||
// the order in which `encode()` writes them; and `when` must remain an
|
||||
// *unsigned* integer, as the big-endian encoding is only order-preserving
|
||||
// for unsigned types.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub(crate) struct ResyncQueueKey {
|
||||
pub(crate) when: u64,
|
||||
pub(crate) hash: Hash,
|
||||
}
|
||||
|
||||
impl db::DbBytes for ResyncQueueKey {
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
let mut v = Vec::with_capacity(40);
|
||||
v.extend_from_slice(&u64::to_be_bytes(self.when));
|
||||
v.extend_from_slice(self.hash.as_slice());
|
||||
v
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> std::result::Result<Self, db::DecodeError> {
|
||||
if bytes.len() != 40 {
|
||||
return Err(db::DecodeError(
|
||||
format!(
|
||||
"invalid resync queue key: expected 40 bytes, got {}",
|
||||
bytes.len()
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
Ok(ResyncQueueKey {
|
||||
when: u64::from_be_bytes(bytes[..8].try_into().unwrap()),
|
||||
hash: Hash::try_from(&bytes[8..])
|
||||
.ok_or_else(|| db::DecodeError("invalid resync queue key: bad hash".into()))?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl db::DbOrdKey for ResyncQueueKey {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy)]
|
||||
struct ResyncPersistedConfig {
|
||||
n_workers: usize,
|
||||
@@ -74,22 +117,21 @@ enum ResyncIterResult {
|
||||
IdleFor(Duration),
|
||||
}
|
||||
|
||||
type BusySet = Arc<Mutex<HashSet<Vec<u8>>>>;
|
||||
type BusySet = Arc<Mutex<HashSet<ResyncQueueKey>>>;
|
||||
|
||||
struct BusyBlock {
|
||||
time_bytes: Vec<u8>,
|
||||
hash_bytes: Vec<u8>,
|
||||
key: ResyncQueueKey,
|
||||
busy_set: BusySet,
|
||||
}
|
||||
|
||||
impl BlockResyncManager {
|
||||
pub(crate) fn new(db: &db::Db, system: &System) -> Self {
|
||||
let queue = db
|
||||
.open_tree("block_local_resync_queue")
|
||||
.open_typed_tree("block_local_resync_queue")
|
||||
.expect("Unable to open block_local_resync_queue tree");
|
||||
|
||||
let errors = db
|
||||
.open_tree("block_local_resync_errors")
|
||||
.open_typed_tree("block_local_resync_errors")
|
||||
.expect("Unable to open block_local_resync_errors tree");
|
||||
|
||||
let persister = PersisterShared::new(&system.metadata_dir, "resync_cfg");
|
||||
@@ -116,11 +158,10 @@ impl BlockResyncManager {
|
||||
/// Clear the error counter for a block and put it in queue immediately
|
||||
pub fn clear_backoff(&self, hash: &Hash) -> Result<(), Error> {
|
||||
let now = now_msec();
|
||||
if let Some(ec) = self.errors.get(hash)? {
|
||||
let mut ec = ErrorCounter::decode(&ec);
|
||||
if let Some(mut ec) = self.errors.get(hash)? {
|
||||
if ec.errors > 0 {
|
||||
ec.last_try = now - ec.delay_msec();
|
||||
self.errors.insert(hash, ec.encode())?;
|
||||
self.errors.insert(hash, &ec)?;
|
||||
self.put_to_resync_at(hash, now)?;
|
||||
return Ok(());
|
||||
}
|
||||
@@ -251,23 +292,21 @@ impl BlockResyncManager {
|
||||
|
||||
pub(crate) fn put_to_resync_at(&self, hash: &Hash, when: u64) -> db::Result<()> {
|
||||
trace!("Put resync_queue: {} {:?}", when, hash);
|
||||
let mut key = u64::to_be_bytes(when).to_vec();
|
||||
key.extend(hash.as_ref());
|
||||
self.queue.insert(key, hash.as_ref())?;
|
||||
let qkey = ResyncQueueKey { when, hash: *hash };
|
||||
self.queue.insert(&qkey, hash)?;
|
||||
self.notify.notify_waiters();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn resync_iter(&self, manager: &BlockManager) -> Result<ResyncIterResult, db::Error> {
|
||||
if let Some(block) = self.get_block_to_resync()? {
|
||||
let time_msec = u64::from_be_bytes(block.time_bytes[0..8].try_into().unwrap());
|
||||
let time_msec = block.key.when;
|
||||
let now = now_msec();
|
||||
|
||||
if now >= time_msec {
|
||||
let hash = Hash::try_from(&block.hash_bytes[..]).unwrap();
|
||||
let hash = block.key.hash;
|
||||
|
||||
if let Some(ec) = self.errors.get(hash.as_slice())? {
|
||||
let ec = ErrorCounter::decode(&ec);
|
||||
if let Some(ec) = self.errors.get(&hash)? {
|
||||
if now < ec.next_try() {
|
||||
// if next retry after an error is not yet,
|
||||
// don't do resync and return early, but still
|
||||
@@ -277,7 +316,7 @@ impl BlockResyncManager {
|
||||
// is not removing the one we added just above
|
||||
// (we want to do the remove after the insert to ensure
|
||||
// that the item is not lost if we crash in-between)
|
||||
self.queue.remove(&block.time_bytes)?;
|
||||
self.queue.remove(&block.key)?;
|
||||
return Ok(ResyncIterResult::BusyDidNothing);
|
||||
}
|
||||
}
|
||||
@@ -307,21 +346,21 @@ impl BlockResyncManager {
|
||||
manager.metrics.resync_error_counter.add(1);
|
||||
error!("Error when resyncing {:?}: {}", hash, e);
|
||||
|
||||
let err_counter = match self.errors.get(hash.as_slice())? {
|
||||
Some(ec) => ErrorCounter::decode(&ec).add1(now + 1),
|
||||
let err_counter = match self.errors.get(&hash)? {
|
||||
Some(ec) => ec.add1(now + 1),
|
||||
None => ErrorCounter::new(now + 1),
|
||||
};
|
||||
|
||||
self.errors.insert(hash.as_slice(), err_counter.encode())?;
|
||||
self.errors.insert(&hash, &err_counter)?;
|
||||
|
||||
self.put_to_resync_at(&hash, err_counter.next_try())?;
|
||||
// err_counter.next_try() >= now + 1 > now,
|
||||
// the entry we remove from the queue is not
|
||||
// the entry we inserted with put_to_resync_at
|
||||
self.queue.remove(&block.time_bytes)?;
|
||||
self.queue.remove(&block.key)?;
|
||||
} else {
|
||||
self.errors.remove(hash.as_slice())?;
|
||||
self.queue.remove(&block.time_bytes)?;
|
||||
self.errors.remove(&hash)?;
|
||||
self.queue.remove(&block.key)?;
|
||||
}
|
||||
|
||||
Ok(ResyncIterResult::BusyDidSomething)
|
||||
@@ -344,12 +383,11 @@ impl BlockResyncManager {
|
||||
fn get_block_to_resync(&self) -> Result<Option<BusyBlock>, db::Error> {
|
||||
let mut busy = self.busy_set.lock().unwrap();
|
||||
for it in self.queue.iter()? {
|
||||
let (time_bytes, hash_bytes) = it?;
|
||||
if !busy.contains(&time_bytes) {
|
||||
busy.insert(time_bytes.clone());
|
||||
let (key, _) = it?;
|
||||
if !busy.contains(&key) {
|
||||
busy.insert(key);
|
||||
return Ok(Some(BusyBlock {
|
||||
time_bytes,
|
||||
hash_bytes,
|
||||
key,
|
||||
busy_set: self.busy_set.clone(),
|
||||
}));
|
||||
}
|
||||
@@ -506,7 +544,7 @@ impl BlockResyncManager {
|
||||
impl Drop for BusyBlock {
|
||||
fn drop(&mut self) {
|
||||
let mut busy = self.busy_set.lock().unwrap();
|
||||
busy.remove(&self.time_bytes);
|
||||
busy.remove(&self.key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,6 +554,7 @@ pub(crate) struct ResyncWorker {
|
||||
tranquilizer: Tranquilizer,
|
||||
next_delay: Duration,
|
||||
persister: PersisterShared<ResyncPersistedConfig>,
|
||||
had_decode_error: bool,
|
||||
}
|
||||
|
||||
impl ResyncWorker {
|
||||
@@ -527,6 +566,7 @@ impl ResyncWorker {
|
||||
tranquilizer: Tranquilizer::new(30),
|
||||
next_delay: Duration::from_secs(10),
|
||||
persister,
|
||||
had_decode_error: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -574,6 +614,15 @@ impl Worker for ResyncWorker {
|
||||
self.next_delay = delay;
|
||||
Ok(WorkerState::Idle)
|
||||
}
|
||||
Err(db::Error::Decode(e)) => {
|
||||
// We give it one second chance in the very unlikely case that the bytes would somehow
|
||||
// have been corrupted during read and that a new read would lead to a correct decoding.
|
||||
if self.had_decode_error {
|
||||
panic!("An error has happened when decoding something stored in the local k/v store: {}.", e);
|
||||
}
|
||||
self.had_decode_error = true;
|
||||
Ok(WorkerState::Busy)
|
||||
}
|
||||
Err(e) => {
|
||||
// The errors that we have here are only db errors
|
||||
// We don't really know how to handle them so just ¯\_(ツ)_/¯
|
||||
@@ -581,6 +630,7 @@ impl Worker for ResyncWorker {
|
||||
// if it does there is not much we can do -- TODO should we just panic?)
|
||||
// Here we just give the error to the worker manager,
|
||||
// it will print it to the logs and increment a counter
|
||||
self.had_decode_error = false;
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
@@ -602,6 +652,7 @@ impl Worker for ResyncWorker {
|
||||
|
||||
/// Counts the number of errors when resyncing a block,
|
||||
/// and the time of the last try.
|
||||
///
|
||||
/// Used to implement exponential backoff.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct ErrorCounter {
|
||||
@@ -609,6 +660,31 @@ pub(crate) struct ErrorCounter {
|
||||
pub(crate) last_try: u64,
|
||||
}
|
||||
|
||||
impl db::DbBytes for ErrorCounter {
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
let mut v = Vec::with_capacity(16);
|
||||
v.extend_from_slice(&u64::to_be_bytes(self.errors));
|
||||
v.extend_from_slice(&u64::to_be_bytes(self.last_try));
|
||||
v
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> std::result::Result<Self, db::DecodeError> {
|
||||
if bytes.len() != 16 {
|
||||
return Err(db::DecodeError(
|
||||
format!(
|
||||
"invalid error counter: expected 16 bytes, got {}",
|
||||
bytes.len()
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
Ok(Self {
|
||||
errors: u64::from_be_bytes(bytes[..8].try_into().unwrap()),
|
||||
last_try: u64::from_be_bytes(bytes[8..].try_into().unwrap()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorCounter {
|
||||
fn new(now: u64) -> Self {
|
||||
Self {
|
||||
@@ -617,21 +693,6 @@ impl ErrorCounter {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn decode(data: &[u8]) -> Self {
|
||||
Self {
|
||||
errors: u64::from_be_bytes(data[0..8].try_into().unwrap()),
|
||||
last_try: u64::from_be_bytes(data[8..16].try_into().unwrap()),
|
||||
}
|
||||
}
|
||||
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
[
|
||||
u64::to_be_bytes(self.errors),
|
||||
u64::to_be_bytes(self.last_try),
|
||||
]
|
||||
.concat()
|
||||
}
|
||||
|
||||
fn add1(self, now: u64) -> Self {
|
||||
Self {
|
||||
errors: self.errors + 1,
|
||||
|
||||
+56
-50
@@ -12,7 +12,7 @@ use fjall::{
|
||||
|
||||
use crate::{
|
||||
open::{Engine, OpenOpt},
|
||||
Db, Error, IDb, ITx, ITxFn, OnCommit, Result, TxError, TxFnResult, TxOpError, TxOpResult,
|
||||
Db, DbError, DbResult, Error, IDb, ITx, ITxFn, OnCommit, TxError, TxFnResult, TxOpError,
|
||||
TxResult, TxValueIter, Value, ValueIter,
|
||||
};
|
||||
|
||||
@@ -20,10 +20,10 @@ pub use fjall;
|
||||
|
||||
// --
|
||||
|
||||
pub(crate) fn open_db(path: &Path, opt: &OpenOpt) -> Result<Db> {
|
||||
pub(crate) fn open_db(path: &Path, opt: &OpenOpt) -> DbResult<Db> {
|
||||
info!("Opening Fjall database at: {}", path.display());
|
||||
if opt.fsync {
|
||||
return Err(Error(
|
||||
return Err(DbError(
|
||||
"metadata_fsync is not supported with the Fjall database engine".into(),
|
||||
));
|
||||
}
|
||||
@@ -37,21 +37,27 @@ pub(crate) fn open_db(path: &Path, opt: &OpenOpt) -> Result<Db> {
|
||||
|
||||
// -- err
|
||||
|
||||
impl From<fjall::Error> for Error {
|
||||
fn from(e: fjall::Error) -> Error {
|
||||
Error(format!("fjall: {}", e).into())
|
||||
impl From<fjall::Error> for DbError {
|
||||
fn from(e: fjall::Error) -> DbError {
|
||||
DbError(format!("fjall: {}", e).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<fjall::LsmError> for Error {
|
||||
fn from(e: fjall::LsmError) -> Error {
|
||||
Error(format!("fjall lsm_tree: {}", e).into())
|
||||
impl From<fjall::LsmError> for DbError {
|
||||
fn from(e: fjall::LsmError) -> DbError {
|
||||
DbError(format!("fjall lsm_tree: {}", e).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<fjall::Error> for Error {
|
||||
fn from(e: fjall::Error) -> Error {
|
||||
Error::Db(DbError::from(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<fjall::Error> for TxOpError {
|
||||
fn from(e: fjall::Error) -> TxOpError {
|
||||
TxOpError(e.into())
|
||||
DbError::from(e).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,11 +82,11 @@ impl FjallDb {
|
||||
fn get_tree(
|
||||
&self,
|
||||
i: usize,
|
||||
) -> Result<MappedRwLockReadGuard<'_, TransactionalPartitionHandle>> {
|
||||
) -> DbResult<MappedRwLockReadGuard<'_, TransactionalPartitionHandle>> {
|
||||
RwLockReadGuard::try_map(self.trees.read(), |trees: &Vec<_>| {
|
||||
trees.get(i).map(|tup| &tup.1)
|
||||
})
|
||||
.map_err(|_| Error("invalid tree id".into()))
|
||||
.map_err(|_| DbError("invalid tree id".into()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +95,7 @@ impl IDb for FjallDb {
|
||||
"Fjall (EXPERIMENTAL!)".into()
|
||||
}
|
||||
|
||||
fn open_tree(&self, name: &str) -> Result<usize> {
|
||||
fn open_tree(&self, name: &str) -> DbResult<usize> {
|
||||
let mut trees = self.trees.write();
|
||||
let safe_name = encode_name(name)?;
|
||||
if let Some(i) = trees.iter().position(|(name, _)| *name == safe_name) {
|
||||
@@ -104,15 +110,15 @@ impl IDb for FjallDb {
|
||||
}
|
||||
}
|
||||
|
||||
fn list_trees(&self) -> Result<Vec<String>> {
|
||||
fn list_trees(&self) -> DbResult<Vec<String>> {
|
||||
self.keyspace
|
||||
.list_partitions()
|
||||
.iter()
|
||||
.map(|n| decode_name(n))
|
||||
.collect::<Result<Vec<_>>>()
|
||||
.collect::<DbResult<Vec<_>>>()
|
||||
}
|
||||
|
||||
fn snapshot(&self, base_path: &Path) -> Result<()> {
|
||||
fn snapshot(&self, base_path: &Path) -> DbResult<()> {
|
||||
std::fs::create_dir_all(base_path)?;
|
||||
let path = Engine::Fjall.db_path(base_path);
|
||||
|
||||
@@ -138,7 +144,7 @@ impl IDb for FjallDb {
|
||||
|
||||
// ----
|
||||
|
||||
fn get(&self, tree_idx: usize, key: &[u8]) -> Result<Option<Value>> {
|
||||
fn get(&self, tree_idx: usize, key: &[u8]) -> DbResult<Option<Value>> {
|
||||
let tree = self.get_tree(tree_idx)?;
|
||||
let tx = self.keyspace.read_tx();
|
||||
let val = tx.get(&tree, key)?;
|
||||
@@ -148,17 +154,17 @@ impl IDb for FjallDb {
|
||||
}
|
||||
}
|
||||
|
||||
fn approximate_len(&self, tree_idx: usize) -> Result<usize> {
|
||||
fn approximate_len(&self, tree_idx: usize) -> DbResult<usize> {
|
||||
let tree = self.get_tree(tree_idx)?;
|
||||
Ok(tree.approximate_len())
|
||||
}
|
||||
fn is_empty(&self, tree_idx: usize) -> Result<bool> {
|
||||
fn is_empty(&self, tree_idx: usize) -> DbResult<bool> {
|
||||
let tree = self.get_tree(tree_idx)?;
|
||||
let tx = self.keyspace.read_tx();
|
||||
Ok(tx.is_empty(&tree)?)
|
||||
}
|
||||
|
||||
fn insert(&self, tree_idx: usize, key: &[u8], value: &[u8]) -> Result<()> {
|
||||
fn insert(&self, tree_idx: usize, key: &[u8], value: &[u8]) -> DbResult<()> {
|
||||
let tree = self.get_tree(tree_idx)?;
|
||||
let mut tx = self.keyspace.write_tx();
|
||||
tx.insert(&tree, key, value);
|
||||
@@ -166,7 +172,7 @@ impl IDb for FjallDb {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove(&self, tree_idx: usize, key: &[u8]) -> Result<()> {
|
||||
fn remove(&self, tree_idx: usize, key: &[u8]) -> DbResult<()> {
|
||||
let tree = self.get_tree(tree_idx)?;
|
||||
let mut tx = self.keyspace.write_tx();
|
||||
tx.remove(&tree, key);
|
||||
@@ -174,11 +180,11 @@ impl IDb for FjallDb {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&self, tree_idx: usize) -> Result<()> {
|
||||
fn clear(&self, tree_idx: usize) -> DbResult<()> {
|
||||
let mut trees = self.trees.write();
|
||||
|
||||
if tree_idx >= trees.len() {
|
||||
return Err(Error("invalid tree id".into()));
|
||||
return Err(DbError("invalid tree id".into()));
|
||||
}
|
||||
let (name, tree) = trees.remove(tree_idx);
|
||||
|
||||
@@ -191,13 +197,13 @@ impl IDb for FjallDb {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn iter(&self, tree_idx: usize) -> Result<ValueIter<'_>> {
|
||||
fn iter(&self, tree_idx: usize) -> DbResult<ValueIter<'_>> {
|
||||
let tree = self.get_tree(tree_idx)?;
|
||||
let tx = self.keyspace.read_tx();
|
||||
Ok(Box::new(tx.iter(&tree).map(iterator_remap)))
|
||||
}
|
||||
|
||||
fn iter_rev(&self, tree_idx: usize) -> Result<ValueIter<'_>> {
|
||||
fn iter_rev(&self, tree_idx: usize) -> DbResult<ValueIter<'_>> {
|
||||
let tree = self.get_tree(tree_idx)?;
|
||||
let tx = self.keyspace.read_tx();
|
||||
Ok(Box::new(tx.iter(&tree).rev().map(iterator_remap)))
|
||||
@@ -208,7 +214,7 @@ impl IDb for FjallDb {
|
||||
tree_idx: usize,
|
||||
low: Bound<&'r [u8]>,
|
||||
high: Bound<&'r [u8]>,
|
||||
) -> Result<ValueIter<'_>> {
|
||||
) -> DbResult<ValueIter<'_>> {
|
||||
let tree = self.get_tree(tree_idx)?;
|
||||
let tx = self.keyspace.read_tx();
|
||||
Ok(Box::new(
|
||||
@@ -221,7 +227,7 @@ impl IDb for FjallDb {
|
||||
tree_idx: usize,
|
||||
low: Bound<&'r [u8]>,
|
||||
high: Bound<&'r [u8]>,
|
||||
) -> Result<ValueIter<'_>> {
|
||||
) -> DbResult<ValueIter<'_>> {
|
||||
let tree = self.get_tree(tree_idx)?;
|
||||
let tx = self.keyspace.read_tx();
|
||||
Ok(Box::new(
|
||||
@@ -252,9 +258,9 @@ impl IDb for FjallDb {
|
||||
}
|
||||
TxFnResult::DbErr => {
|
||||
tx.tx.rollback();
|
||||
Err(TxError::Db(Error(
|
||||
"(this message will be discarded)".into(),
|
||||
)))
|
||||
Err(TxError::Db(
|
||||
DbError("(this message will be discarded)".into()).into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -268,47 +274,47 @@ struct FjallTx<'a> {
|
||||
}
|
||||
|
||||
impl<'a> FjallTx<'a> {
|
||||
fn get_tree(&self, i: usize) -> TxOpResult<&TransactionalPartitionHandle> {
|
||||
fn get_tree(&self, i: usize) -> DbResult<&TransactionalPartitionHandle> {
|
||||
self.trees.get(i).map(|tup| &tup.1).ok_or_else(|| {
|
||||
TxOpError(Error(
|
||||
DbError(
|
||||
"invalid tree id (it might have been opened after the transaction started)".into(),
|
||||
))
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ITx for FjallTx<'a> {
|
||||
fn get(&self, tree_idx: usize, key: &[u8]) -> TxOpResult<Option<Value>> {
|
||||
fn get(&self, tree_idx: usize, key: &[u8]) -> DbResult<Option<Value>> {
|
||||
let tree = self.get_tree(tree_idx)?;
|
||||
match self.tx.get(tree, key)? {
|
||||
Some(v) => Ok(Some(v.to_vec())),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
fn len(&self, tree_idx: usize) -> TxOpResult<usize> {
|
||||
fn len(&self, tree_idx: usize) -> DbResult<usize> {
|
||||
let tree = self.get_tree(tree_idx)?;
|
||||
Ok(self.tx.len(tree)?)
|
||||
}
|
||||
|
||||
fn insert(&mut self, tree_idx: usize, key: &[u8], value: &[u8]) -> TxOpResult<()> {
|
||||
fn insert(&mut self, tree_idx: usize, key: &[u8], value: &[u8]) -> DbResult<()> {
|
||||
let tree = self.get_tree(tree_idx)?.clone();
|
||||
self.tx.insert(&tree, key, value);
|
||||
Ok(())
|
||||
}
|
||||
fn remove(&mut self, tree_idx: usize, key: &[u8]) -> TxOpResult<()> {
|
||||
fn remove(&mut self, tree_idx: usize, key: &[u8]) -> DbResult<()> {
|
||||
let tree = self.get_tree(tree_idx)?.clone();
|
||||
self.tx.remove(&tree, key);
|
||||
Ok(())
|
||||
}
|
||||
fn clear(&mut self, _tree_idx: usize) -> TxOpResult<()> {
|
||||
fn clear(&mut self, _tree_idx: usize) -> DbResult<()> {
|
||||
unimplemented!("LSM tree clearing in cross-partition transaction is not supported")
|
||||
}
|
||||
|
||||
fn iter(&self, tree_idx: usize) -> TxOpResult<TxValueIter<'_>> {
|
||||
fn iter(&self, tree_idx: usize) -> DbResult<TxValueIter<'_>> {
|
||||
let tree = self.get_tree(tree_idx)?.clone();
|
||||
Ok(Box::new(self.tx.iter(&tree).map(iterator_remap_tx)))
|
||||
}
|
||||
fn iter_rev(&self, tree_idx: usize) -> TxOpResult<TxValueIter<'_>> {
|
||||
fn iter_rev(&self, tree_idx: usize) -> DbResult<TxValueIter<'_>> {
|
||||
let tree = self.get_tree(tree_idx)?.clone();
|
||||
Ok(Box::new(self.tx.iter(&tree).rev().map(iterator_remap_tx)))
|
||||
}
|
||||
@@ -318,7 +324,7 @@ impl<'a> ITx for FjallTx<'a> {
|
||||
tree_idx: usize,
|
||||
low: Bound<&'r [u8]>,
|
||||
high: Bound<&'r [u8]>,
|
||||
) -> TxOpResult<TxValueIter<'_>> {
|
||||
) -> DbResult<TxValueIter<'_>> {
|
||||
let tree = self.get_tree(tree_idx)?;
|
||||
let low = clone_bound(low);
|
||||
let high = clone_bound(high);
|
||||
@@ -333,7 +339,7 @@ impl<'a> ITx for FjallTx<'a> {
|
||||
tree_idx: usize,
|
||||
low: Bound<&'r [u8]>,
|
||||
high: Bound<&'r [u8]>,
|
||||
) -> TxOpResult<TxValueIter<'_>> {
|
||||
) -> DbResult<TxValueIter<'_>> {
|
||||
let tree = self.get_tree(tree_idx)?;
|
||||
let low = clone_bound(low);
|
||||
let high = clone_bound(high);
|
||||
@@ -348,14 +354,14 @@ impl<'a> ITx for FjallTx<'a> {
|
||||
|
||||
// -- maps fjall's (k, v) to ours
|
||||
|
||||
fn iterator_remap(r: fjall::Result<(fjall::Slice, fjall::Slice)>) -> Result<(Value, Value)> {
|
||||
fn iterator_remap(r: fjall::Result<(fjall::Slice, fjall::Slice)>) -> DbResult<(Value, Value)> {
|
||||
r.map(|(k, v)| (k.to_vec(), v.to_vec()))
|
||||
.map_err(|e| e.into())
|
||||
.map_err(DbError::from)
|
||||
}
|
||||
|
||||
fn iterator_remap_tx(r: fjall::Result<(fjall::Slice, fjall::Slice)>) -> TxOpResult<(Value, Value)> {
|
||||
fn iterator_remap_tx(r: fjall::Result<(fjall::Slice, fjall::Slice)>) -> DbResult<(Value, Value)> {
|
||||
r.map(|(k, v)| (k.to_vec(), v.to_vec()))
|
||||
.map_err(|e| e.into())
|
||||
.map_err(DbError::from)
|
||||
}
|
||||
|
||||
// -- utils to deal with Garage's tightness on Bound lifetimes
|
||||
@@ -378,7 +384,7 @@ fn clone_bound(bound: Bound<&[u8]>) -> ByteVecBound {
|
||||
|
||||
// -- utils to encode table names --
|
||||
|
||||
fn encode_name(s: &str) -> Result<String> {
|
||||
fn encode_name(s: &str) -> DbResult<String> {
|
||||
let base = 'A' as u32;
|
||||
|
||||
let mut ret = String::with_capacity(s.len() + 10);
|
||||
@@ -392,7 +398,7 @@ fn encode_name(s: &str) -> Result<String> {
|
||||
ret.push(char::from_u32(base + c_hi).unwrap());
|
||||
ret.push(char::from_u32(base + c_lo).unwrap());
|
||||
} else {
|
||||
return Err(Error(
|
||||
return Err(DbError(
|
||||
format!("table name {} could not be safely encoded", s).into(),
|
||||
));
|
||||
}
|
||||
@@ -400,10 +406,10 @@ fn encode_name(s: &str) -> Result<String> {
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
fn decode_name(s: &str) -> Result<String> {
|
||||
fn decode_name(s: &str) -> DbResult<String> {
|
||||
use std::convert::TryFrom;
|
||||
|
||||
let errfn = || Error(format!("encoded table name {} is invalid", s).into());
|
||||
let errfn = || DbError(format!("encoded table name {} is invalid", s).into());
|
||||
let c_map = |c: char| {
|
||||
let c = c as u32;
|
||||
let base = 'A' as u32;
|
||||
|
||||
+97
-57
@@ -9,6 +9,7 @@ pub mod lmdb_adapter;
|
||||
pub mod sqlite_adapter;
|
||||
|
||||
pub mod open;
|
||||
pub mod typed;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test;
|
||||
@@ -23,6 +24,7 @@ use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
|
||||
pub use open::*;
|
||||
pub use typed::{DbBytes, DbOrdKey, TypedIter, TypedTree, TypedTxIter};
|
||||
|
||||
pub(crate) type OnCommit = Vec<Box<dyn FnOnce()>>;
|
||||
|
||||
@@ -38,21 +40,34 @@ pub struct Transaction<'a> {
|
||||
pub struct Tree(Arc<dyn IDb>, usize);
|
||||
|
||||
pub type Value = Vec<u8>;
|
||||
pub type ValueIter<'a> = Box<dyn std::iter::Iterator<Item = Result<(Value, Value)>> + 'a>;
|
||||
pub type TxValueIter<'a> = Box<dyn std::iter::Iterator<Item = TxOpResult<(Value, Value)>> + 'a>;
|
||||
pub type ValueIter<'a> = Box<dyn std::iter::Iterator<Item = DbResult<(Value, Value)>> + 'a>;
|
||||
pub type TxValueIter<'a> = Box<dyn std::iter::Iterator<Item = DbResult<(Value, Value)>> + 'a>;
|
||||
|
||||
// ----
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("{0}")]
|
||||
pub struct Error(pub Cow<'static, str>);
|
||||
#[error("database error: {0}")]
|
||||
pub struct DbError(pub Cow<'static, str>);
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(e: std::io::Error) -> Error {
|
||||
Error(format!("IO: {}", e).into())
|
||||
#[derive(Debug, Error)]
|
||||
#[error("decode error: {0}")]
|
||||
pub struct DecodeError(pub Cow<'static, str>);
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Db(#[from] DbError),
|
||||
#[error(transparent)]
|
||||
Decode(#[from] DecodeError),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for DbError {
|
||||
fn from(e: std::io::Error) -> DbError {
|
||||
DbError(format!("IO: {}", e).into())
|
||||
}
|
||||
}
|
||||
|
||||
pub type DbResult<T> = std::result::Result<T, DbError>;
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -60,6 +75,18 @@ pub type Result<T> = std::result::Result<T, Error>;
|
||||
pub struct TxOpError(pub(crate) Error);
|
||||
pub type TxOpResult<T> = std::result::Result<T, TxOpError>;
|
||||
|
||||
impl From<DbError> for TxOpError {
|
||||
fn from(e: DbError) -> TxOpError {
|
||||
TxOpError(e.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DecodeError> for TxOpError {
|
||||
fn from(e: DecodeError) -> TxOpError {
|
||||
TxOpError(e.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TxError<E> {
|
||||
Abort(E),
|
||||
@@ -73,6 +100,12 @@ impl<E> From<TxOpError> for TxError<E> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> From<DbError> for TxError<E> {
|
||||
fn from(e: DbError) -> TxError<E> {
|
||||
TxError::Db(e.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unabort<R, E>(res: TxResult<R, E>) -> TxOpResult<std::result::Result<R, E>> {
|
||||
match res {
|
||||
Ok(v) => Ok(Ok(v)),
|
||||
@@ -88,12 +121,12 @@ impl Db {
|
||||
self.0.engine()
|
||||
}
|
||||
|
||||
pub fn open_tree<S: AsRef<str>>(&self, name: S) -> Result<Tree> {
|
||||
pub fn open_tree<S: AsRef<str>>(&self, name: S) -> DbResult<Tree> {
|
||||
let tree_id = self.0.open_tree(name.as_ref())?;
|
||||
Ok(Tree(self.0.clone(), tree_id))
|
||||
}
|
||||
|
||||
pub fn list_trees(&self) -> Result<Vec<String>> {
|
||||
pub fn list_trees(&self) -> DbResult<Vec<String>> {
|
||||
self.0.list_trees()
|
||||
}
|
||||
|
||||
@@ -147,27 +180,28 @@ impl Db {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snapshot(&self, path: &Path) -> Result<()> {
|
||||
pub fn snapshot(&self, path: &Path) -> DbResult<()> {
|
||||
self.0.snapshot(path)
|
||||
}
|
||||
|
||||
pub fn import(&self, other: &Db) -> Result<()> {
|
||||
let existing_trees = self.list_trees()?;
|
||||
if !existing_trees.is_empty() {
|
||||
return Err(Error(
|
||||
return Err(DbError(
|
||||
format!(
|
||||
"destination database already contains data: {:?}",
|
||||
existing_trees
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let tree_names = other.list_trees()?;
|
||||
for name in tree_names {
|
||||
let tree = self.open_tree(&name)?;
|
||||
if !tree.is_empty()? {
|
||||
return Err(Error(format!("tree {} already contains data", name).into()));
|
||||
return Err(DbError(format!("tree {} already contains data", name).into()).into());
|
||||
}
|
||||
|
||||
let ex_tree = other.open_tree(&name)?;
|
||||
@@ -186,7 +220,7 @@ impl Db {
|
||||
});
|
||||
let total = match tx_res {
|
||||
Err(TxError::Db(e)) => return Err(e),
|
||||
Err(TxError::Abort(e)) => return Err(e),
|
||||
Err(TxError::Abort(e)) => return Err(e.into()),
|
||||
Ok(x) => x,
|
||||
};
|
||||
|
||||
@@ -204,24 +238,24 @@ impl Tree {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get<T: AsRef<[u8]>>(&self, key: T) -> Result<Option<Value>> {
|
||||
pub fn get<T: AsRef<[u8]>>(&self, key: T) -> DbResult<Option<Value>> {
|
||||
self.0.get(self.1, key.as_ref())
|
||||
}
|
||||
#[inline]
|
||||
pub fn approximate_len(&self) -> Result<usize> {
|
||||
pub fn approximate_len(&self) -> DbResult<usize> {
|
||||
self.0.approximate_len(self.1)
|
||||
}
|
||||
#[inline]
|
||||
pub fn is_empty(&self) -> Result<bool> {
|
||||
pub fn is_empty(&self) -> DbResult<bool> {
|
||||
self.0.is_empty(self.1)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn first(&self) -> Result<Option<(Value, Value)>> {
|
||||
pub fn first(&self) -> DbResult<Option<(Value, Value)>> {
|
||||
self.iter()?.next().transpose()
|
||||
}
|
||||
#[inline]
|
||||
pub fn get_gt<T: AsRef<[u8]>>(&self, from: T) -> Result<Option<(Value, Value)>> {
|
||||
pub fn get_gt<T: AsRef<[u8]>>(&self, from: T) -> DbResult<Option<(Value, Value)>> {
|
||||
if from.as_ref().is_empty() {
|
||||
self.iter()?.next().transpose()
|
||||
} else {
|
||||
@@ -233,31 +267,31 @@ impl Tree {
|
||||
|
||||
/// Returns the old value if there was one
|
||||
#[inline]
|
||||
pub fn insert<T: AsRef<[u8]>, U: AsRef<[u8]>>(&self, key: T, value: U) -> Result<()> {
|
||||
pub fn insert<T: AsRef<[u8]>, U: AsRef<[u8]>>(&self, key: T, value: U) -> DbResult<()> {
|
||||
self.0.insert(self.1, key.as_ref(), value.as_ref())
|
||||
}
|
||||
/// Returns the old value if there was one
|
||||
#[inline]
|
||||
pub fn remove<T: AsRef<[u8]>>(&self, key: T) -> Result<()> {
|
||||
pub fn remove<T: AsRef<[u8]>>(&self, key: T) -> DbResult<()> {
|
||||
self.0.remove(self.1, key.as_ref())
|
||||
}
|
||||
/// Clears all values from the tree
|
||||
#[inline]
|
||||
pub fn clear(&self) -> Result<()> {
|
||||
pub fn clear(&self) -> DbResult<()> {
|
||||
self.0.clear(self.1)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn iter(&self) -> Result<ValueIter<'_>> {
|
||||
pub fn iter(&self) -> DbResult<ValueIter<'_>> {
|
||||
self.0.iter(self.1)
|
||||
}
|
||||
#[inline]
|
||||
pub fn iter_rev(&self) -> Result<ValueIter<'_>> {
|
||||
pub fn iter_rev(&self) -> DbResult<ValueIter<'_>> {
|
||||
self.0.iter_rev(self.1)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn range<K, R>(&self, range: R) -> Result<ValueIter<'_>>
|
||||
pub fn range<K, R>(&self, range: R) -> DbResult<ValueIter<'_>>
|
||||
where
|
||||
K: AsRef<[u8]>,
|
||||
R: RangeBounds<K>,
|
||||
@@ -267,7 +301,7 @@ impl Tree {
|
||||
self.0.range(self.1, get_bound(sb), get_bound(eb))
|
||||
}
|
||||
#[inline]
|
||||
pub fn range_rev<K, R>(&self, range: R) -> Result<ValueIter<'_>>
|
||||
pub fn range_rev<K, R>(&self, range: R) -> DbResult<ValueIter<'_>>
|
||||
where
|
||||
K: AsRef<[u8]>,
|
||||
R: RangeBounds<K>,
|
||||
@@ -282,11 +316,11 @@ impl Tree {
|
||||
impl<'a> Transaction<'a> {
|
||||
#[inline]
|
||||
pub fn get<T: AsRef<[u8]>>(&self, tree: &Tree, key: T) -> TxOpResult<Option<Value>> {
|
||||
self.tx.get(tree.1, key.as_ref())
|
||||
self.tx.get(tree.1, key.as_ref()).map_err(Into::into)
|
||||
}
|
||||
#[inline]
|
||||
pub fn len(&self, tree: &Tree) -> TxOpResult<usize> {
|
||||
self.tx.len(tree.1)
|
||||
self.tx.len(tree.1).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns the old value if there was one
|
||||
@@ -297,26 +331,28 @@ impl<'a> Transaction<'a> {
|
||||
key: T,
|
||||
value: U,
|
||||
) -> TxOpResult<()> {
|
||||
self.tx.insert(tree.1, key.as_ref(), value.as_ref())
|
||||
self.tx
|
||||
.insert(tree.1, key.as_ref(), value.as_ref())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
/// Returns the old value if there was one
|
||||
#[inline]
|
||||
pub fn remove<T: AsRef<[u8]>>(&mut self, tree: &Tree, key: T) -> TxOpResult<()> {
|
||||
self.tx.remove(tree.1, key.as_ref())
|
||||
self.tx.remove(tree.1, key.as_ref()).map_err(Into::into)
|
||||
}
|
||||
/// Clears all values in a tree
|
||||
#[inline]
|
||||
pub fn clear(&mut self, tree: &Tree) -> TxOpResult<()> {
|
||||
self.tx.clear(tree.1)
|
||||
self.tx.clear(tree.1).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn iter(&self, tree: &Tree) -> TxOpResult<TxValueIter<'_>> {
|
||||
self.tx.iter(tree.1)
|
||||
self.tx.iter(tree.1).map_err(Into::into)
|
||||
}
|
||||
#[inline]
|
||||
pub fn iter_rev(&self, tree: &Tree) -> TxOpResult<TxValueIter<'_>> {
|
||||
self.tx.iter_rev(tree.1)
|
||||
self.tx.iter_rev(tree.1).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -327,7 +363,9 @@ impl<'a> Transaction<'a> {
|
||||
{
|
||||
let sb = range.start_bound();
|
||||
let eb = range.end_bound();
|
||||
self.tx.range(tree.1, get_bound(sb), get_bound(eb))
|
||||
self.tx
|
||||
.range(tree.1, get_bound(sb), get_bound(eb))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
#[inline]
|
||||
pub fn range_rev<K, R>(&self, tree: &Tree, range: R) -> TxOpResult<TxValueIter<'_>>
|
||||
@@ -337,7 +375,9 @@ impl<'a> Transaction<'a> {
|
||||
{
|
||||
let sb = range.start_bound();
|
||||
let eb = range.end_bound();
|
||||
self.tx.range_rev(tree.1, get_bound(sb), get_bound(eb))
|
||||
self.tx
|
||||
.range_rev(tree.1, get_bound(sb), get_bound(eb))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -350,60 +390,60 @@ impl<'a> Transaction<'a> {
|
||||
|
||||
pub(crate) trait IDb: Send + Sync {
|
||||
fn engine(&self) -> String;
|
||||
fn open_tree(&self, name: &str) -> Result<usize>;
|
||||
fn list_trees(&self) -> Result<Vec<String>>;
|
||||
fn snapshot(&self, path: &Path) -> Result<()>;
|
||||
fn open_tree(&self, name: &str) -> DbResult<usize>;
|
||||
fn list_trees(&self) -> DbResult<Vec<String>>;
|
||||
fn snapshot(&self, path: &Path) -> DbResult<()>;
|
||||
|
||||
fn get(&self, tree: usize, key: &[u8]) -> Result<Option<Value>>;
|
||||
fn approximate_len(&self, tree: usize) -> Result<usize>;
|
||||
fn is_empty(&self, tree: usize) -> Result<bool>;
|
||||
fn get(&self, tree: usize, key: &[u8]) -> DbResult<Option<Value>>;
|
||||
fn approximate_len(&self, tree: usize) -> DbResult<usize>;
|
||||
fn is_empty(&self, tree: usize) -> DbResult<bool>;
|
||||
|
||||
fn insert(&self, tree: usize, key: &[u8], value: &[u8]) -> Result<()>;
|
||||
fn remove(&self, tree: usize, key: &[u8]) -> Result<()>;
|
||||
fn clear(&self, tree: usize) -> Result<()>;
|
||||
fn insert(&self, tree: usize, key: &[u8], value: &[u8]) -> DbResult<()>;
|
||||
fn remove(&self, tree: usize, key: &[u8]) -> DbResult<()>;
|
||||
fn clear(&self, tree: usize) -> DbResult<()>;
|
||||
|
||||
fn iter(&self, tree: usize) -> Result<ValueIter<'_>>;
|
||||
fn iter_rev(&self, tree: usize) -> Result<ValueIter<'_>>;
|
||||
fn iter(&self, tree: usize) -> DbResult<ValueIter<'_>>;
|
||||
fn iter_rev(&self, tree: usize) -> DbResult<ValueIter<'_>>;
|
||||
|
||||
fn range<'r>(
|
||||
&self,
|
||||
tree: usize,
|
||||
low: Bound<&'r [u8]>,
|
||||
high: Bound<&'r [u8]>,
|
||||
) -> Result<ValueIter<'_>>;
|
||||
) -> DbResult<ValueIter<'_>>;
|
||||
fn range_rev<'r>(
|
||||
&self,
|
||||
tree: usize,
|
||||
low: Bound<&'r [u8]>,
|
||||
high: Bound<&'r [u8]>,
|
||||
) -> Result<ValueIter<'_>>;
|
||||
) -> DbResult<ValueIter<'_>>;
|
||||
|
||||
fn transaction(&self, f: &dyn ITxFn) -> TxResult<OnCommit, ()>;
|
||||
}
|
||||
|
||||
pub(crate) trait ITx {
|
||||
fn get(&self, tree: usize, key: &[u8]) -> TxOpResult<Option<Value>>;
|
||||
fn len(&self, tree: usize) -> TxOpResult<usize>;
|
||||
fn get(&self, tree: usize, key: &[u8]) -> DbResult<Option<Value>>;
|
||||
fn len(&self, tree: usize) -> DbResult<usize>;
|
||||
|
||||
fn insert(&mut self, tree: usize, key: &[u8], value: &[u8]) -> TxOpResult<()>;
|
||||
fn remove(&mut self, tree: usize, key: &[u8]) -> TxOpResult<()>;
|
||||
fn clear(&mut self, tree: usize) -> TxOpResult<()>;
|
||||
fn insert(&mut self, tree: usize, key: &[u8], value: &[u8]) -> DbResult<()>;
|
||||
fn remove(&mut self, tree: usize, key: &[u8]) -> DbResult<()>;
|
||||
fn clear(&mut self, tree: usize) -> DbResult<()>;
|
||||
|
||||
fn iter(&self, tree: usize) -> TxOpResult<TxValueIter<'_>>;
|
||||
fn iter_rev(&self, tree: usize) -> TxOpResult<TxValueIter<'_>>;
|
||||
fn iter(&self, tree: usize) -> DbResult<TxValueIter<'_>>;
|
||||
fn iter_rev(&self, tree: usize) -> DbResult<TxValueIter<'_>>;
|
||||
|
||||
fn range<'r>(
|
||||
&self,
|
||||
tree: usize,
|
||||
low: Bound<&'r [u8]>,
|
||||
high: Bound<&'r [u8]>,
|
||||
) -> TxOpResult<TxValueIter<'_>>;
|
||||
) -> DbResult<TxValueIter<'_>>;
|
||||
fn range_rev<'r>(
|
||||
&self,
|
||||
tree: usize,
|
||||
low: Bound<&'r [u8]>,
|
||||
high: Bound<&'r [u8]>,
|
||||
) -> TxOpResult<TxValueIter<'_>>;
|
||||
) -> DbResult<TxValueIter<'_>>;
|
||||
}
|
||||
|
||||
pub(crate) trait ITxFn {
|
||||
|
||||
+49
-43
@@ -14,7 +14,7 @@ type Database = heed::Database<Bytes, Bytes>;
|
||||
|
||||
use crate::{
|
||||
open::{Engine, OpenOpt},
|
||||
Db, Error, IDb, ITx, ITxFn, OnCommit, Result, TxError, TxFnResult, TxOpError, TxOpResult,
|
||||
Db, DbError, DbResult, Error, IDb, ITx, ITxFn, OnCommit, TxError, TxFnResult, TxOpError,
|
||||
TxResult, TxValueIter, Value, ValueIter,
|
||||
};
|
||||
|
||||
@@ -22,10 +22,10 @@ pub use heed;
|
||||
|
||||
// ---- top-level open function
|
||||
|
||||
pub(crate) fn open_db(path: &PathBuf, opt: &OpenOpt) -> Result<Db> {
|
||||
pub(crate) fn open_db(path: &PathBuf, opt: &OpenOpt) -> DbResult<Db> {
|
||||
info!("Opening LMDB database at: {}", path.display());
|
||||
if let Err(e) = std::fs::create_dir_all(path) {
|
||||
return Err(Error(
|
||||
return Err(DbError(
|
||||
format!("Unable to create LMDB data directory: {}", e).into(),
|
||||
));
|
||||
}
|
||||
@@ -48,7 +48,7 @@ pub(crate) fn open_db(path: &PathBuf, opt: &OpenOpt) -> Result<Db> {
|
||||
env_builder.open(path)
|
||||
};
|
||||
match open_res {
|
||||
Err(heed::Error::Io(e)) if e.kind() == std::io::ErrorKind::OutOfMemory => Err(Error(
|
||||
Err(heed::Error::Io(e)) if e.kind() == std::io::ErrorKind::OutOfMemory => Err(DbError(
|
||||
"OutOfMemory error while trying to open LMDB database. This can happen \
|
||||
if your operating system is not allowing you to use sufficient virtual \
|
||||
memory address space. Please check that no limit is set (ulimit -v). \
|
||||
@@ -56,22 +56,28 @@ pub(crate) fn open_db(path: &PathBuf, opt: &OpenOpt) -> Result<Db> {
|
||||
On 32-bit machines, you should probably switch to another database engine."
|
||||
.into(),
|
||||
)),
|
||||
Err(e) => Err(Error(format!("Cannot open LMDB database: {}", e).into())),
|
||||
Err(e) => Err(DbError(format!("Cannot open LMDB database: {}", e).into())),
|
||||
Ok(db) => Ok(LmdbDb::init(db)),
|
||||
}
|
||||
}
|
||||
|
||||
// -- err
|
||||
|
||||
impl From<heed::Error> for DbError {
|
||||
fn from(e: heed::Error) -> DbError {
|
||||
DbError(format!("LMDB: {}", e).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<heed::Error> for Error {
|
||||
fn from(e: heed::Error) -> Error {
|
||||
Error(format!("LMDB: {}", e).into())
|
||||
DbError::from(e).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<heed::Error> for TxOpError {
|
||||
fn from(e: heed::Error) -> TxOpError {
|
||||
TxOpError(e.into())
|
||||
DbError::from(e).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,14 +97,14 @@ impl LmdbDb {
|
||||
Db(Arc::new(s))
|
||||
}
|
||||
|
||||
fn get_tree(&self, i: usize) -> Result<Database> {
|
||||
fn get_tree(&self, i: usize) -> DbResult<Database> {
|
||||
self.trees
|
||||
.read()
|
||||
.unwrap()
|
||||
.0
|
||||
.get(i)
|
||||
.cloned()
|
||||
.ok_or_else(|| Error("invalid tree id".into()))
|
||||
.ok_or_else(|| DbError("invalid tree id".into()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +113,7 @@ impl IDb for LmdbDb {
|
||||
"LMDB (using Heed crate)".into()
|
||||
}
|
||||
|
||||
fn open_tree(&self, name: &str) -> Result<usize> {
|
||||
fn open_tree(&self, name: &str) -> DbResult<usize> {
|
||||
let mut trees = self.trees.write().unwrap();
|
||||
if let Some(i) = trees.1.get(name) {
|
||||
Ok(*i)
|
||||
@@ -122,7 +128,7 @@ impl IDb for LmdbDb {
|
||||
}
|
||||
}
|
||||
|
||||
fn list_trees(&self) -> Result<Vec<String>> {
|
||||
fn list_trees(&self) -> DbResult<Vec<String>> {
|
||||
let rtxn = self.db.read_txn()?;
|
||||
let tree0 = match self
|
||||
.db
|
||||
@@ -153,7 +159,7 @@ impl IDb for LmdbDb {
|
||||
Ok(ret2)
|
||||
}
|
||||
|
||||
fn snapshot(&self, base_path: &Path) -> Result<()> {
|
||||
fn snapshot(&self, base_path: &Path) -> DbResult<()> {
|
||||
std::fs::create_dir_all(base_path)?;
|
||||
let path = Engine::Lmdb.db_path(base_path);
|
||||
self.db
|
||||
@@ -163,7 +169,7 @@ impl IDb for LmdbDb {
|
||||
|
||||
// ----
|
||||
|
||||
fn get(&self, tree: usize, key: &[u8]) -> Result<Option<Value>> {
|
||||
fn get(&self, tree: usize, key: &[u8]) -> DbResult<Option<Value>> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
|
||||
let tx = self.db.read_txn()?;
|
||||
@@ -174,18 +180,18 @@ impl IDb for LmdbDb {
|
||||
}
|
||||
}
|
||||
|
||||
fn approximate_len(&self, tree: usize) -> Result<usize> {
|
||||
fn approximate_len(&self, tree: usize) -> DbResult<usize> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
let tx = self.db.read_txn()?;
|
||||
Ok(tree.len(&tx)?.try_into().unwrap())
|
||||
}
|
||||
fn is_empty(&self, tree: usize) -> Result<bool> {
|
||||
fn is_empty(&self, tree: usize) -> DbResult<bool> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
let tx = self.db.read_txn()?;
|
||||
Ok(tree.is_empty(&tx)?)
|
||||
}
|
||||
|
||||
fn insert(&self, tree: usize, key: &[u8], value: &[u8]) -> Result<()> {
|
||||
fn insert(&self, tree: usize, key: &[u8], value: &[u8]) -> DbResult<()> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
let mut tx = self.db.write_txn()?;
|
||||
tree.put(&mut tx, key, value)?;
|
||||
@@ -193,7 +199,7 @@ impl IDb for LmdbDb {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove(&self, tree: usize, key: &[u8]) -> Result<()> {
|
||||
fn remove(&self, tree: usize, key: &[u8]) -> DbResult<()> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
let mut tx = self.db.write_txn()?;
|
||||
tree.delete(&mut tx, key)?;
|
||||
@@ -201,7 +207,7 @@ impl IDb for LmdbDb {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&self, tree: usize) -> Result<()> {
|
||||
fn clear(&self, tree: usize) -> DbResult<()> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
let mut tx = self.db.write_txn()?;
|
||||
tree.clear(&mut tx)?;
|
||||
@@ -209,14 +215,14 @@ impl IDb for LmdbDb {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn iter(&self, tree: usize) -> Result<ValueIter<'_>> {
|
||||
fn iter(&self, tree: usize) -> DbResult<ValueIter<'_>> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
let tx = self.db.read_txn()?;
|
||||
// Safety: the cloture does not store its argument anywhere,
|
||||
unsafe { TxAndIterator::make(tx, |tx| Ok(tree.iter(tx)?)) }
|
||||
}
|
||||
|
||||
fn iter_rev(&self, tree: usize) -> Result<ValueIter<'_>> {
|
||||
fn iter_rev(&self, tree: usize) -> DbResult<ValueIter<'_>> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
let tx = self.db.read_txn()?;
|
||||
// Safety: the cloture does not store its argument anywhere,
|
||||
@@ -228,7 +234,7 @@ impl IDb for LmdbDb {
|
||||
tree: usize,
|
||||
low: Bound<&'r [u8]>,
|
||||
high: Bound<&'r [u8]>,
|
||||
) -> Result<ValueIter<'_>> {
|
||||
) -> DbResult<ValueIter<'_>> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
let tx = self.db.read_txn()?;
|
||||
// Safety: the cloture does not store its argument anywhere,
|
||||
@@ -239,7 +245,7 @@ impl IDb for LmdbDb {
|
||||
tree: usize,
|
||||
low: Bound<&'r [u8]>,
|
||||
high: Bound<&'r [u8]>,
|
||||
) -> Result<ValueIter<'_>> {
|
||||
) -> DbResult<ValueIter<'_>> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
let tx = self.db.read_txn()?;
|
||||
// Safety: the cloture does not store its argument anywhere,
|
||||
@@ -271,9 +277,9 @@ impl IDb for LmdbDb {
|
||||
}
|
||||
TxFnResult::DbErr => {
|
||||
tx.tx.abort();
|
||||
Err(TxError::Db(Error(
|
||||
"(this message will be discarded)".into(),
|
||||
)))
|
||||
Err(TxError::Db(
|
||||
DbError("(this message will be discarded)".into()).into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,49 +293,49 @@ struct LmdbTx<'a> {
|
||||
}
|
||||
|
||||
impl<'a> LmdbTx<'a> {
|
||||
fn get_tree(&self, i: usize) -> TxOpResult<&Database> {
|
||||
fn get_tree(&self, i: usize) -> DbResult<&Database> {
|
||||
self.trees.get(i).ok_or_else(|| {
|
||||
TxOpError(Error(
|
||||
DbError(
|
||||
"invalid tree id (it might have been opened after the transaction started)".into(),
|
||||
))
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ITx for LmdbTx<'a> {
|
||||
fn get(&self, tree: usize, key: &[u8]) -> TxOpResult<Option<Value>> {
|
||||
fn get(&self, tree: usize, key: &[u8]) -> DbResult<Option<Value>> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
match tree.get(&self.tx, key)? {
|
||||
Some(v) => Ok(Some(v.to_vec())),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
fn len(&self, tree: usize) -> TxOpResult<usize> {
|
||||
fn len(&self, tree: usize) -> DbResult<usize> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
Ok(tree.len(&self.tx)? as usize)
|
||||
}
|
||||
|
||||
fn insert(&mut self, tree: usize, key: &[u8], value: &[u8]) -> TxOpResult<()> {
|
||||
fn insert(&mut self, tree: usize, key: &[u8], value: &[u8]) -> DbResult<()> {
|
||||
let tree = *self.get_tree(tree)?;
|
||||
tree.put(&mut self.tx, key, value)?;
|
||||
Ok(())
|
||||
}
|
||||
fn remove(&mut self, tree: usize, key: &[u8]) -> TxOpResult<()> {
|
||||
fn remove(&mut self, tree: usize, key: &[u8]) -> DbResult<()> {
|
||||
let tree = *self.get_tree(tree)?;
|
||||
tree.delete(&mut self.tx, key)?;
|
||||
Ok(())
|
||||
}
|
||||
fn clear(&mut self, tree: usize) -> TxOpResult<()> {
|
||||
fn clear(&mut self, tree: usize) -> DbResult<()> {
|
||||
let tree = *self.get_tree(tree)?;
|
||||
tree.clear(&mut self.tx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn iter(&self, tree: usize) -> TxOpResult<TxValueIter<'_>> {
|
||||
fn iter(&self, tree: usize) -> DbResult<TxValueIter<'_>> {
|
||||
let tree = *self.get_tree(tree)?;
|
||||
Ok(Box::new(tree.iter(&self.tx)?.map(tx_iter_item)))
|
||||
}
|
||||
fn iter_rev(&self, tree: usize) -> TxOpResult<TxValueIter<'_>> {
|
||||
fn iter_rev(&self, tree: usize) -> DbResult<TxValueIter<'_>> {
|
||||
let tree = *self.get_tree(tree)?;
|
||||
Ok(Box::new(tree.rev_iter(&self.tx)?.map(tx_iter_item)))
|
||||
}
|
||||
@@ -339,7 +345,7 @@ impl<'a> ITx for LmdbTx<'a> {
|
||||
tree: usize,
|
||||
low: Bound<&'r [u8]>,
|
||||
high: Bound<&'r [u8]>,
|
||||
) -> TxOpResult<TxValueIter<'_>> {
|
||||
) -> DbResult<TxValueIter<'_>> {
|
||||
let tree = *self.get_tree(tree)?;
|
||||
Ok(Box::new(
|
||||
tree.range(&self.tx, &(low, high))?.map(tx_iter_item),
|
||||
@@ -350,7 +356,7 @@ impl<'a> ITx for LmdbTx<'a> {
|
||||
tree: usize,
|
||||
low: Bound<&'r [u8]>,
|
||||
high: Bound<&'r [u8]>,
|
||||
) -> TxOpResult<TxValueIter<'_>> {
|
||||
) -> DbResult<TxValueIter<'_>> {
|
||||
let tree = *self.get_tree(tree)?;
|
||||
Ok(Box::new(
|
||||
tree.rev_range(&self.tx, &(low, high))?.map(tx_iter_item),
|
||||
@@ -386,9 +392,9 @@ where
|
||||
}
|
||||
|
||||
/// Safety: iterfun must not store its argument anywhere but in its result.
|
||||
unsafe fn make<F>(tx: RoTxn<'a, WithTls>, iterfun: F) -> Result<ValueIter<'a>>
|
||||
unsafe fn make<F>(tx: RoTxn<'a, WithTls>, iterfun: F) -> DbResult<ValueIter<'a>>
|
||||
where
|
||||
F: FnOnce(&'a RoTxn<'a>) -> Result<I>,
|
||||
F: FnOnce(&'a RoTxn<'a>) -> DbResult<I>,
|
||||
{
|
||||
let res = TxAndIterator {
|
||||
tx,
|
||||
@@ -436,13 +442,13 @@ impl<'a, I> Iterator for TxAndIteratorPin<'a, I>
|
||||
where
|
||||
I: Iterator<Item = IteratorItem<'a>> + 'a,
|
||||
{
|
||||
type Item = Result<(Value, Value)>;
|
||||
type Item = DbResult<(Value, Value)>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut_ref = Pin::as_mut(&mut self.0);
|
||||
let next = mut_ref.iter().as_mut()?.next()?;
|
||||
let res = match next {
|
||||
Err(e) => Err(e.into()),
|
||||
Err(e) => Err(DbError::from(e)),
|
||||
Ok((k, v)) => Ok((k.to_vec(), v.to_vec())),
|
||||
};
|
||||
Some(res)
|
||||
@@ -453,9 +459,9 @@ where
|
||||
|
||||
fn tx_iter_item<'a>(
|
||||
item: std::result::Result<(&'a [u8], &'a [u8]), heed::Error>,
|
||||
) -> TxOpResult<(Vec<u8>, Vec<u8>)> {
|
||||
) -> DbResult<(Vec<u8>, Vec<u8>)> {
|
||||
item.map(|(k, v)| (k.to_vec(), v.to_vec()))
|
||||
.map_err(|e| TxOpError(Error::from(e)))
|
||||
.map_err(DbError::from)
|
||||
}
|
||||
|
||||
// ---- utility ----
|
||||
|
||||
+10
-9
@@ -1,6 +1,6 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::{Db, Error, Result};
|
||||
use crate::{Db, DbError, Error, Result};
|
||||
|
||||
/// List of supported database engine types
|
||||
///
|
||||
@@ -49,14 +49,14 @@ impl std::str::FromStr for Engine {
|
||||
"lmdb" | "heed" => Ok(Self::Lmdb),
|
||||
"sqlite" | "sqlite3" | "rusqlite" => Ok(Self::Sqlite),
|
||||
"fjall" => Ok(Self::Fjall),
|
||||
"sled" => Err(Error("Sled is no longer supported as a database engine. Converting your old metadata db can be done using an older Garage binary (e.g. v0.9.4).".into())),
|
||||
kind => Err(Error(
|
||||
"sled" => Err(DbError("Sled is no longer supported as a database engine. Converting your old metadata db can be done using an older Garage binary (e.g. v0.9.4).".into()).into()),
|
||||
kind => Err(DbError(
|
||||
format!(
|
||||
"Invalid DB engine: {} (options are: lmdb, sqlite, fjall)",
|
||||
kind
|
||||
)
|
||||
.into(),
|
||||
)),
|
||||
).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,22 +72,23 @@ pub fn open_db(path: &PathBuf, engine: Engine, opt: &OpenOpt) -> Result<Db> {
|
||||
match engine {
|
||||
// ---- Sqlite DB ----
|
||||
#[cfg(feature = "sqlite")]
|
||||
Engine::Sqlite => crate::sqlite_adapter::open_db(path, opt),
|
||||
Engine::Sqlite => Ok(crate::sqlite_adapter::open_db(path, opt)?),
|
||||
|
||||
// ---- LMDB DB ----
|
||||
#[cfg(feature = "lmdb")]
|
||||
Engine::Lmdb => crate::lmdb_adapter::open_db(path, opt),
|
||||
Engine::Lmdb => Ok(crate::lmdb_adapter::open_db(path, opt)?),
|
||||
|
||||
// ---- Fjall DB ----
|
||||
#[cfg(feature = "fjall")]
|
||||
Engine::Fjall => crate::fjall_adapter::open_db(path, opt),
|
||||
Engine::Fjall => Ok(crate::fjall_adapter::open_db(path, opt)?),
|
||||
|
||||
// Pattern is unreachable when all supported DB engines are compiled into binary. The allow
|
||||
// attribute is added so that we won't have to change this match in case stop building
|
||||
// support for one or more engines by default.
|
||||
#[allow(unreachable_patterns)]
|
||||
engine => Err(Error(
|
||||
engine => Err(DbError(
|
||||
format!("DB engine support not available in this build: {}", engine).into(),
|
||||
)),
|
||||
)
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
+55
-43
@@ -12,7 +12,7 @@ use rusqlite::{params, Rows, Statement, Transaction};
|
||||
|
||||
use crate::{
|
||||
open::{Engine, OpenOpt},
|
||||
Db, Error, IDb, ITx, ITxFn, OnCommit, Result, TxError, TxFnResult, TxOpError, TxOpResult,
|
||||
Db, DbError, DbResult, Error, IDb, ITx, ITxFn, OnCommit, TxError, TxFnResult, TxOpError,
|
||||
TxResult, TxValueIter, Value, ValueIter,
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ pub use rusqlite;
|
||||
|
||||
// ---- top-level open function
|
||||
|
||||
pub(crate) fn open_db(path: &PathBuf, opt: &OpenOpt) -> Result<Db> {
|
||||
pub(crate) fn open_db(path: &PathBuf, opt: &OpenOpt) -> DbResult<Db> {
|
||||
info!("Opening Sqlite database at: {}", path.display());
|
||||
let manager = r2d2_sqlite::SqliteConnectionManager::file(path);
|
||||
SqliteDb::open(manager, opt.fsync)
|
||||
@@ -32,21 +32,33 @@ type Connection = r2d2::PooledConnection<SqliteConnectionManager>;
|
||||
|
||||
// --- err
|
||||
|
||||
impl From<rusqlite::Error> for DbError {
|
||||
fn from(e: rusqlite::Error) -> DbError {
|
||||
DbError(format!("Sqlite: {}", e).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<rusqlite::Error> for Error {
|
||||
fn from(e: rusqlite::Error) -> Error {
|
||||
Error(format!("Sqlite: {}", e).into())
|
||||
DbError::from(e).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<r2d2::Error> for DbError {
|
||||
fn from(e: r2d2::Error) -> DbError {
|
||||
DbError(format!("Sqlite: {}", e).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<r2d2::Error> for Error {
|
||||
fn from(e: r2d2::Error) -> Error {
|
||||
Error(format!("Sqlite: {}", e).into())
|
||||
DbError::from(e).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<rusqlite::Error> for TxOpError {
|
||||
fn from(e: rusqlite::Error) -> TxOpError {
|
||||
TxOpError(e.into())
|
||||
DbError::from(e).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +74,7 @@ pub struct SqliteDb {
|
||||
}
|
||||
|
||||
impl SqliteDb {
|
||||
pub fn open(manager: SqliteConnectionManager, sync_mode: bool) -> Result<Db> {
|
||||
pub fn open(manager: SqliteConnectionManager, sync_mode: bool) -> DbResult<Db> {
|
||||
let manager = manager.with_init(move |db| {
|
||||
db.pragma_update(None, "journal_mode", "WAL")?;
|
||||
if sync_mode {
|
||||
@@ -82,16 +94,16 @@ impl SqliteDb {
|
||||
}
|
||||
|
||||
impl SqliteDb {
|
||||
fn get_tree(&self, i: usize) -> Result<Arc<str>> {
|
||||
fn get_tree(&self, i: usize) -> DbResult<Arc<str>> {
|
||||
self.trees
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(i)
|
||||
.cloned()
|
||||
.ok_or_else(|| Error("invalid tree id".into()))
|
||||
.ok_or_else(|| DbError("invalid tree id".into()))
|
||||
}
|
||||
|
||||
fn internal_get(&self, db: &Connection, tree: &str, key: &[u8]) -> Result<Option<Value>> {
|
||||
fn internal_get(&self, db: &Connection, tree: &str, key: &[u8]) -> DbResult<Option<Value>> {
|
||||
let mut stmt = db.prepare(&format!("SELECT v FROM {} WHERE k = ?1", tree))?;
|
||||
let mut res_iter = stmt.query([key])?;
|
||||
match res_iter.next()? {
|
||||
@@ -106,7 +118,7 @@ impl IDb for SqliteDb {
|
||||
format!("sqlite3 v{} (using rusqlite crate)", rusqlite::version())
|
||||
}
|
||||
|
||||
fn open_tree(&self, name: &str) -> Result<usize> {
|
||||
fn open_tree(&self, name: &str) -> DbResult<usize> {
|
||||
let name = format!("tree_{}", name.replace(':', "_COLON_"));
|
||||
let mut trees = self.trees.write().unwrap();
|
||||
|
||||
@@ -133,7 +145,7 @@ impl IDb for SqliteDb {
|
||||
}
|
||||
}
|
||||
|
||||
fn list_trees(&self) -> Result<Vec<String>> {
|
||||
fn list_trees(&self) -> DbResult<Vec<String>> {
|
||||
let mut trees = vec![];
|
||||
|
||||
let db = self.db.get()?;
|
||||
@@ -150,13 +162,13 @@ impl IDb for SqliteDb {
|
||||
Ok(trees)
|
||||
}
|
||||
|
||||
fn snapshot(&self, base_path: &Path) -> Result<()> {
|
||||
fn snapshot(&self, base_path: &Path) -> DbResult<()> {
|
||||
std::fs::create_dir_all(base_path)?;
|
||||
let path = Engine::Sqlite
|
||||
.db_path(base_path)
|
||||
.into_os_string()
|
||||
.into_string()
|
||||
.map_err(|_| Error("invalid sqlite path string".into()))?;
|
||||
.map_err(|_| DbError("invalid sqlite path string".into()))?;
|
||||
|
||||
info!("Start sqlite VACUUM INTO `{}`", path);
|
||||
self.db.get()?.execute("VACUUM INTO ?1", params![path])?;
|
||||
@@ -167,12 +179,12 @@ impl IDb for SqliteDb {
|
||||
|
||||
// ----
|
||||
|
||||
fn get(&self, tree: usize, key: &[u8]) -> Result<Option<Value>> {
|
||||
fn get(&self, tree: usize, key: &[u8]) -> DbResult<Option<Value>> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
self.internal_get(&self.db.get()?, &tree, key)
|
||||
}
|
||||
|
||||
fn approximate_len(&self, tree: usize) -> Result<usize> {
|
||||
fn approximate_len(&self, tree: usize) -> DbResult<usize> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
let db = self.db.get()?;
|
||||
|
||||
@@ -184,11 +196,11 @@ impl IDb for SqliteDb {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty(&self, tree: usize) -> Result<bool> {
|
||||
fn is_empty(&self, tree: usize) -> DbResult<bool> {
|
||||
Ok(self.approximate_len(tree)? == 0)
|
||||
}
|
||||
|
||||
fn insert(&self, tree: usize, key: &[u8], value: &[u8]) -> Result<()> {
|
||||
fn insert(&self, tree: usize, key: &[u8], value: &[u8]) -> DbResult<()> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
let db = self.db.get()?;
|
||||
let lock = self.write_lock.lock();
|
||||
@@ -206,7 +218,7 @@ impl IDb for SqliteDb {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove(&self, tree: usize, key: &[u8]) -> Result<()> {
|
||||
fn remove(&self, tree: usize, key: &[u8]) -> DbResult<()> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
let db = self.db.get()?;
|
||||
let lock = self.write_lock.lock();
|
||||
@@ -217,7 +229,7 @@ impl IDb for SqliteDb {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&self, tree: usize) -> Result<()> {
|
||||
fn clear(&self, tree: usize) -> DbResult<()> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
let db = self.db.get()?;
|
||||
let lock = self.write_lock.lock();
|
||||
@@ -228,13 +240,13 @@ impl IDb for SqliteDb {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn iter(&self, tree: usize) -> Result<ValueIter<'_>> {
|
||||
fn iter(&self, tree: usize) -> DbResult<ValueIter<'_>> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
let sql = format!("SELECT k, v FROM {} ORDER BY k ASC", tree);
|
||||
DbValueIterator::make(self.db.get()?, &sql, [])
|
||||
}
|
||||
|
||||
fn iter_rev(&self, tree: usize) -> Result<ValueIter<'_>> {
|
||||
fn iter_rev(&self, tree: usize) -> DbResult<ValueIter<'_>> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
let sql = format!("SELECT k, v FROM {} ORDER BY k DESC", tree);
|
||||
DbValueIterator::make(self.db.get()?, &sql, [])
|
||||
@@ -245,7 +257,7 @@ impl IDb for SqliteDb {
|
||||
tree: usize,
|
||||
low: Bound<&'r [u8]>,
|
||||
high: Bound<&'r [u8]>,
|
||||
) -> Result<ValueIter<'_>> {
|
||||
) -> DbResult<ValueIter<'_>> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
|
||||
let (bounds_sql, params) = bounds_sql(low, high);
|
||||
@@ -263,7 +275,7 @@ impl IDb for SqliteDb {
|
||||
tree: usize,
|
||||
low: Bound<&'r [u8]>,
|
||||
high: Bound<&'r [u8]>,
|
||||
) -> Result<ValueIter<'_>> {
|
||||
) -> DbResult<ValueIter<'_>> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
|
||||
let (bounds_sql, params) = bounds_sql(low, high);
|
||||
@@ -300,9 +312,9 @@ impl IDb for SqliteDb {
|
||||
}
|
||||
TxFnResult::DbErr => {
|
||||
tx.tx.rollback().map_err(Error::from).map_err(TxError::Db)?;
|
||||
Err(TxError::Db(Error(
|
||||
"(this message will be discarded)".into(),
|
||||
)))
|
||||
Err(TxError::Db(
|
||||
DbError("(this message will be discarded)".into()).into(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
@@ -320,15 +332,15 @@ struct SqliteTx<'a> {
|
||||
}
|
||||
|
||||
impl<'a> SqliteTx<'a> {
|
||||
fn get_tree(&self, i: usize) -> TxOpResult<&'_ str> {
|
||||
fn get_tree(&self, i: usize) -> DbResult<&'_ str> {
|
||||
self.trees.get(i).map(Arc::as_ref).ok_or_else(|| {
|
||||
TxOpError(Error(
|
||||
DbError(
|
||||
"invalid tree id (it might have been opened after the transaction started)".into(),
|
||||
))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn internal_get(&self, tree: &str, key: &[u8]) -> TxOpResult<Option<Value>> {
|
||||
fn internal_get(&self, tree: &str, key: &[u8]) -> DbResult<Option<Value>> {
|
||||
let mut stmt = self
|
||||
.tx
|
||||
.prepare(&format!("SELECT v FROM {} WHERE k = ?1", tree))?;
|
||||
@@ -341,11 +353,11 @@ impl<'a> SqliteTx<'a> {
|
||||
}
|
||||
|
||||
impl<'a> ITx for SqliteTx<'a> {
|
||||
fn get(&self, tree: usize, key: &[u8]) -> TxOpResult<Option<Value>> {
|
||||
fn get(&self, tree: usize, key: &[u8]) -> DbResult<Option<Value>> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
self.internal_get(tree, key)
|
||||
}
|
||||
fn len(&self, tree: usize) -> TxOpResult<usize> {
|
||||
fn len(&self, tree: usize) -> DbResult<usize> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
let mut stmt = self.tx.prepare(&format!("SELECT COUNT(*) FROM {}", tree))?;
|
||||
let mut res_iter = stmt.query([])?;
|
||||
@@ -355,30 +367,30 @@ impl<'a> ITx for SqliteTx<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn insert(&mut self, tree: usize, key: &[u8], value: &[u8]) -> TxOpResult<()> {
|
||||
fn insert(&mut self, tree: usize, key: &[u8], value: &[u8]) -> DbResult<()> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
let sql = format!("INSERT OR REPLACE INTO {} (k, v) VALUES (?1, ?2)", tree);
|
||||
self.tx.execute(&sql, params![key, value])?;
|
||||
Ok(())
|
||||
}
|
||||
fn remove(&mut self, tree: usize, key: &[u8]) -> TxOpResult<()> {
|
||||
fn remove(&mut self, tree: usize, key: &[u8]) -> DbResult<()> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
self.tx
|
||||
.execute(&format!("DELETE FROM {} WHERE k = ?1", tree), params![key])?;
|
||||
Ok(())
|
||||
}
|
||||
fn clear(&mut self, tree: usize) -> TxOpResult<()> {
|
||||
fn clear(&mut self, tree: usize) -> DbResult<()> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
self.tx.execute(&format!("DELETE FROM {}", tree), [])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn iter(&self, tree: usize) -> TxOpResult<TxValueIter<'_>> {
|
||||
fn iter(&self, tree: usize) -> DbResult<TxValueIter<'_>> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
let sql = format!("SELECT k, v FROM {} ORDER BY k ASC", tree);
|
||||
TxValueIterator::make(self, &sql, [])
|
||||
}
|
||||
fn iter_rev(&self, tree: usize) -> TxOpResult<TxValueIter<'_>> {
|
||||
fn iter_rev(&self, tree: usize) -> DbResult<TxValueIter<'_>> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
let sql = format!("SELECT k, v FROM {} ORDER BY k DESC", tree);
|
||||
TxValueIterator::make(self, &sql, [])
|
||||
@@ -389,7 +401,7 @@ impl<'a> ITx for SqliteTx<'a> {
|
||||
tree: usize,
|
||||
low: Bound<&'r [u8]>,
|
||||
high: Bound<&'r [u8]>,
|
||||
) -> TxOpResult<TxValueIter<'_>> {
|
||||
) -> DbResult<TxValueIter<'_>> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
|
||||
let (bounds_sql, params) = bounds_sql(low, high);
|
||||
@@ -407,7 +419,7 @@ impl<'a> ITx for SqliteTx<'a> {
|
||||
tree: usize,
|
||||
low: Bound<&'r [u8]>,
|
||||
high: Bound<&'r [u8]>,
|
||||
) -> TxOpResult<TxValueIter<'_>> {
|
||||
) -> DbResult<TxValueIter<'_>> {
|
||||
let tree = self.get_tree(tree)?;
|
||||
|
||||
let (bounds_sql, params) = bounds_sql(low, high);
|
||||
@@ -439,7 +451,7 @@ impl DbValueIterator {
|
||||
db: Connection,
|
||||
sql: &str,
|
||||
args: P,
|
||||
) -> Result<ValueIter<'res>> {
|
||||
) -> DbResult<ValueIter<'res>> {
|
||||
let res = DbValueIterator {
|
||||
db,
|
||||
stmt: None,
|
||||
@@ -484,7 +496,7 @@ impl Drop for DbValueIterator {
|
||||
struct DbValueIteratorPin(Pin<Box<DbValueIterator>>);
|
||||
|
||||
impl Iterator for DbValueIteratorPin {
|
||||
type Item = Result<(Value, Value)>;
|
||||
type Item = DbResult<(Value, Value)>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut_ref = Pin::as_mut(&mut self.0);
|
||||
@@ -509,7 +521,7 @@ impl<'a> TxValueIterator<'a> {
|
||||
tx: &'a SqliteTx<'a>,
|
||||
sql: &str,
|
||||
args: P,
|
||||
) -> TxOpResult<TxValueIter<'a>> {
|
||||
) -> DbResult<TxValueIter<'a>> {
|
||||
let stmt = tx.tx.prepare(sql)?;
|
||||
let res = TxValueIterator {
|
||||
stmt,
|
||||
@@ -543,7 +555,7 @@ impl<'a> Drop for TxValueIterator<'a> {
|
||||
struct TxValueIteratorPin<'a>(Pin<Box<TxValueIterator<'a>>>);
|
||||
|
||||
impl<'a> Iterator for TxValueIteratorPin<'a> {
|
||||
type Item = TxOpResult<(Value, Value)>;
|
||||
type Item = DbResult<(Value, Value)>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut_ref = Pin::as_mut(&mut self.0);
|
||||
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::{Bound, RangeBounds};
|
||||
|
||||
// Todo: some parts of this code, notably around ranges are never used but are here to prepare
|
||||
// the migration of the parts of the codebase that still use untyped trees. At one point, this
|
||||
// migration should be done or these functions deleted.
|
||||
|
||||
use super::{
|
||||
DbResult, DecodeError, Error, Result, Transaction, Tree, TxOpError, TxOpResult, TxValueIter,
|
||||
ValueIter,
|
||||
};
|
||||
|
||||
pub use super::Db;
|
||||
|
||||
pub trait DbBytes: Sized {
|
||||
fn encode(&self) -> Vec<u8>;
|
||||
fn decode(bytes: &[u8]) -> std::result::Result<Self, DecodeError>;
|
||||
}
|
||||
|
||||
/// Subtrait of [`DbBytes`] for types used as tree keys with operations where order matters
|
||||
/// (`get_gt`, range, etc...).
|
||||
///
|
||||
/// Implementors must guarantee that the byte encoding is order-preserving:
|
||||
/// for any `a, b: Self`, `a.cmp(&b) == a.encode().cmp(&b.encode())`.
|
||||
pub trait DbOrdKey: DbBytes + Ord {}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TypedTree<K, V> {
|
||||
inner: Tree,
|
||||
_phantom: PhantomData<[(K, V)]>,
|
||||
}
|
||||
|
||||
impl<K: DbBytes, V: DbBytes> TypedTree<K, V> {
|
||||
pub fn new(tree: Tree) -> Self {
|
||||
Self {
|
||||
inner: tree,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn db(&self) -> Db {
|
||||
self.inner.db()
|
||||
}
|
||||
|
||||
pub fn untyped(&self) -> &Tree {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &K) -> Result<Option<V>> {
|
||||
self.inner
|
||||
.get(key.encode())?
|
||||
.map(|v| V::decode(&v).map_err(Error::from))
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub fn approximate_len(&self) -> DbResult<usize> {
|
||||
self.inner.approximate_len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> DbResult<bool> {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
|
||||
pub fn insert(&self, key: &K, value: &V) -> DbResult<()> {
|
||||
self.inner.insert(key.encode(), value.encode())
|
||||
}
|
||||
|
||||
pub fn remove(&self, key: &K) -> DbResult<()> {
|
||||
self.inner.remove(key.encode())
|
||||
}
|
||||
|
||||
pub fn clear(&self) -> DbResult<()> {
|
||||
self.inner.clear()
|
||||
}
|
||||
|
||||
pub fn tx_get(&self, tx: &Transaction<'_>, key: &K) -> TxOpResult<Option<V>> {
|
||||
tx.get(&self.inner, key.encode())?
|
||||
.map(|v| V::decode(&v).map_err(TxOpError::from))
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub fn tx_insert(&self, tx: &mut Transaction<'_>, key: &K, value: &V) -> TxOpResult<()> {
|
||||
tx.insert(&self.inner, key.encode(), value.encode())
|
||||
}
|
||||
|
||||
pub fn tx_remove(&self, tx: &mut Transaction<'_>, key: &K) -> TxOpResult<()> {
|
||||
tx.remove(&self.inner, key.encode())
|
||||
}
|
||||
|
||||
pub fn tx_clear(&self, tx: &mut Transaction<'_>) -> TxOpResult<()> {
|
||||
tx.clear(&self.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: DbOrdKey, V: DbBytes> TypedTree<K, V> {
|
||||
pub fn first(&self) -> Result<Option<(K, V)>> {
|
||||
self.iter()?.next().transpose()
|
||||
}
|
||||
|
||||
pub fn get_gt(&self, from: &K) -> Result<Option<(K, V)>> {
|
||||
self.inner
|
||||
.get_gt(from.encode())?
|
||||
.map(|(k, v)| {
|
||||
Ok((
|
||||
K::decode(&k).map_err(Error::from)?,
|
||||
V::decode(&v).map_err(Error::from)?,
|
||||
))
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> Result<TypedIter<'_, K, V>> {
|
||||
Ok(TypedIter::new(self.inner.iter()?))
|
||||
}
|
||||
|
||||
pub fn iter_rev(&self) -> Result<TypedIter<'_, K, V>> {
|
||||
Ok(TypedIter::new(self.inner.iter_rev()?))
|
||||
}
|
||||
|
||||
pub fn range<R: RangeBounds<K>>(&self, range: R) -> Result<TypedIter<'_, K, V>> {
|
||||
Ok(TypedIter::new(self.inner.range(encode_range(range))?))
|
||||
}
|
||||
|
||||
pub fn range_rev<R: RangeBounds<K>>(&self, range: R) -> Result<TypedIter<'_, K, V>> {
|
||||
Ok(TypedIter::new(self.inner.range_rev(encode_range(range))?))
|
||||
}
|
||||
|
||||
pub fn tx_iter<'t>(&self, tx: &'t Transaction<'_>) -> TxOpResult<TypedTxIter<'t, K, V>> {
|
||||
Ok(TypedTxIter::new(tx.iter(&self.inner)?))
|
||||
}
|
||||
|
||||
pub fn tx_iter_rev<'t>(&self, tx: &'t Transaction<'_>) -> TxOpResult<TypedTxIter<'t, K, V>> {
|
||||
Ok(TypedTxIter::new(tx.iter_rev(&self.inner)?))
|
||||
}
|
||||
|
||||
pub fn tx_range<'t, R: RangeBounds<K>>(
|
||||
&self,
|
||||
tx: &'t Transaction<'_>,
|
||||
range: R,
|
||||
) -> TxOpResult<TypedTxIter<'t, K, V>> {
|
||||
Ok(TypedTxIter::new(
|
||||
tx.range(&self.inner, encode_range(range))?,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn tx_range_rev<'t, R: RangeBounds<K>>(
|
||||
&self,
|
||||
tx: &'t Transaction<'_>,
|
||||
range: R,
|
||||
) -> TxOpResult<TypedTxIter<'t, K, V>> {
|
||||
Ok(TypedTxIter::new(
|
||||
tx.range_rev(&self.inner, encode_range(range))?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: DbBytes, V: DbBytes> From<Tree> for TypedTree<K, V> {
|
||||
fn from(tree: Tree) -> Self {
|
||||
Self::new(tree)
|
||||
}
|
||||
}
|
||||
|
||||
impl Db {
|
||||
pub fn open_typed_tree<K: DbBytes, V: DbBytes, S: AsRef<str>>(
|
||||
&self,
|
||||
name: S,
|
||||
) -> DbResult<TypedTree<K, V>> {
|
||||
Ok(TypedTree::new(self.open_tree(name)?))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TypedIter<'a, K, V> {
|
||||
inner: ValueIter<'a>,
|
||||
_phantom: PhantomData<(K, V)>,
|
||||
}
|
||||
|
||||
impl<'a, K, V> TypedIter<'a, K, V> {
|
||||
fn new(inner: ValueIter<'a>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: DbOrdKey, V: DbBytes> Iterator for TypedIter<'_, K, V> {
|
||||
type Item = Result<(K, V)>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next().map(|res| {
|
||||
let (k, v) = res?;
|
||||
Ok((
|
||||
K::decode(&k).map_err(Error::from)?,
|
||||
V::decode(&v).map_err(Error::from)?,
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TypedTxIter<'a, K, V> {
|
||||
inner: TxValueIter<'a>,
|
||||
_phantom: PhantomData<(K, V)>,
|
||||
}
|
||||
|
||||
impl<'a, K, V> TypedTxIter<'a, K, V> {
|
||||
fn new(inner: TxValueIter<'a>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: DbOrdKey, V: DbBytes> Iterator for TypedTxIter<'_, K, V> {
|
||||
type Item = TxOpResult<(K, V)>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next().map(|res| {
|
||||
let (k, v) = res?;
|
||||
Ok((
|
||||
K::decode(&k).map_err(TxOpError::from)?,
|
||||
V::decode(&v).map_err(TxOpError::from)?,
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_range<K: DbOrdKey, R: RangeBounds<K>>(range: R) -> (Bound<Vec<u8>>, Bound<Vec<u8>>) {
|
||||
(
|
||||
encode_bound(range.start_bound()),
|
||||
encode_bound(range.end_bound()),
|
||||
)
|
||||
}
|
||||
|
||||
fn encode_bound<K: DbOrdKey>(bound: Bound<&K>) -> Bound<Vec<u8>> {
|
||||
match bound {
|
||||
Bound::Included(k) => Bound::Included(k.encode()),
|
||||
Bound::Excluded(k) => Bound::Excluded(k.encode()),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
}
|
||||
}
|
||||
@@ -39,8 +39,8 @@ backtrace.workspace = true
|
||||
bytes.workspace = true
|
||||
bytesize.workspace = true
|
||||
chrono.workspace = true
|
||||
fundu-systemd.workspace = true
|
||||
timeago.workspace = true
|
||||
parse_duration.workspace = true
|
||||
hex.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
@@ -49,7 +49,7 @@ pub struct OpenLmdbOpt {
|
||||
|
||||
pub(crate) fn do_conversion(args: ConvertDbOpt) -> Result<()> {
|
||||
if args.input_engine == args.output_engine {
|
||||
return Err(Error("input and output database engine must differ".into()));
|
||||
return Err(DbError("input and output database engine must differ".into()).into());
|
||||
}
|
||||
|
||||
let opt = OpenOpt {
|
||||
|
||||
@@ -393,7 +393,7 @@ impl Cli {
|
||||
&self,
|
||||
opt: CleanupIncompleteUploadsOpt,
|
||||
) -> Result<(), Error> {
|
||||
let older_than = parse_duration::parse::parse(&opt.older_than)
|
||||
let older_than = garage_util::time::parse_duration(&opt.older_than)
|
||||
.ok_or_message("Invalid duration passed for --older-than parameter")?;
|
||||
|
||||
for b in opt.buckets.iter() {
|
||||
|
||||
@@ -226,7 +226,7 @@ impl Cli {
|
||||
|
||||
pub async fn cmd_import_key(&self, opt: KeyImportOpt) -> Result<(), Error> {
|
||||
if !opt.yes {
|
||||
return Err(Error::Message("This command is intended to re-import keys that were previously generated by Garage. If you want to create a new key, use `garage key new` instead. Add the --yes flag if you really want to re-import a key.".to_string()));
|
||||
return Err(Error::Message("This command is intended to re-import keys that were previously generated by Garage. If you want to create a new key, use `garage key create` instead. Add the --yes flag if you really want to re-import a key.".to_string()));
|
||||
}
|
||||
|
||||
let new_key = self
|
||||
|
||||
@@ -169,7 +169,7 @@ pub fn table_list_abbr<T: IntoIterator<Item = S>, S: AsRef<str>>(values: T) -> S
|
||||
pub fn parse_expires_in(expires_in: &Option<String>) -> Result<Option<DateTime<Utc>>, Error> {
|
||||
expires_in
|
||||
.as_ref()
|
||||
.map(|x| parse_duration::parse::parse(x).map(|dur| Utc::now() + dur))
|
||||
.map(|x| garage_util::time::parse_duration(x).map(|dur| Utc::now() + dur))
|
||||
.transpose()
|
||||
.ok_or_message("Invalid duration passed for --expires-in parameter")
|
||||
}
|
||||
|
||||
@@ -494,7 +494,7 @@ pub struct KeyNewOpt {
|
||||
#[structopt(default_value = "Unnamed key")]
|
||||
pub name: String,
|
||||
/// Set an expiration time for the access key
|
||||
/// (see `docs.rs/parse_duration` for date format)
|
||||
/// (see `docs.rs/fundu-systemd` for date format)
|
||||
#[structopt(long = "expires-in")]
|
||||
pub expires_in: Option<String>,
|
||||
}
|
||||
@@ -505,7 +505,7 @@ pub struct KeySetOpt {
|
||||
pub key_pattern: String,
|
||||
|
||||
/// Set an expiration time for the access key
|
||||
/// (see `docs.rs/parse_duration` for date format)
|
||||
/// (see `docs.rs/fundu-systemd` for date format)
|
||||
#[structopt(long = "expires-in")]
|
||||
pub expires_in: Option<String>,
|
||||
/// Set the access key to never expire
|
||||
@@ -616,7 +616,7 @@ pub enum AdminTokenOperation {
|
||||
pub struct AdminTokenCreateOp {
|
||||
/// Set a name for the token
|
||||
pub name: Option<String>,
|
||||
/// Set an expiration time for the token (see `docs.rs/parse_duration` for date
|
||||
/// Set an expiration time for the token (see `docs.rs/fundu-systemd` for date
|
||||
/// format)
|
||||
#[structopt(long = "expires-in")]
|
||||
pub expires_in: Option<String>,
|
||||
@@ -638,7 +638,7 @@ pub struct AdminTokenSetOp {
|
||||
/// Name or prefix of the ID of the token to modify
|
||||
pub api_token: String,
|
||||
|
||||
/// Set an expiration time for the token (see `docs.rs/parse_duration` for date
|
||||
/// Set an expiration time for the token (see `docs.rs/fundu-systemd` for date
|
||||
/// format)
|
||||
#[structopt(long = "expires-in")]
|
||||
pub expires_in: Option<String>,
|
||||
|
||||
@@ -276,6 +276,11 @@ fn init_logging(opt: &Opt) {
|
||||
tracing_subscriber::fmt()
|
||||
.with_writer(std::io::stderr)
|
||||
.with_env_filter(env_filter)
|
||||
.with_ansi(
|
||||
std::env::var("NO_COLOR")
|
||||
.map(|x| x != "0" && !x.eq_ignore_ascii_case("false"))
|
||||
.unwrap_or(true),
|
||||
)
|
||||
.init();
|
||||
}
|
||||
|
||||
|
||||
@@ -137,14 +137,18 @@ fn read_secret_file(file_path: &PathBuf, allow_world_readable: bool) -> Result<S
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
let metadata = std::fs::metadata(file_path)?;
|
||||
let metadata = std::fs::metadata(file_path).map_err(|e| {
|
||||
format!("Failed to read secret file {}: {}", file_path.display(), e)
|
||||
})?;
|
||||
|
||||
if metadata.mode() & 0o077 != 0 {
|
||||
return Err(format!("File {} is world-readable! (mode: 0{:o}, expected 0600)\nRefusing to start until this is fixed, or environment variable GARAGE_ALLOW_WORLD_READABLE_SECRETS is set to true.", file_path.display(), metadata.mode()).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let secret_buf = std::fs::read_to_string(file_path)?;
|
||||
let secret_buf = std::fs::read_to_string(file_path)
|
||||
.map_err(|e| format!("Failed to read secret file {}: {}", file_path.display(), e))?;
|
||||
|
||||
// trim_end: allows for use case such as `echo "$(openssl rand -hex 32)" > somefile`.
|
||||
// also editors sometimes add a trailing newline
|
||||
|
||||
@@ -152,6 +152,14 @@ pub async fn run_server(
|
||||
}
|
||||
}
|
||||
|
||||
// Deregister from Consul (if enabled) in the background, in parallel with the
|
||||
// rest of the shutdown sequence, so that it doesn't add to shutdown latency.
|
||||
#[cfg(feature = "consul-discovery")]
|
||||
let deregister_consul_task = tokio::spawn({
|
||||
let system = garage.system.clone();
|
||||
async move { system.deregister_from_discovery().await }
|
||||
});
|
||||
|
||||
// Remove RPC handlers for system to break reference cycles
|
||||
info!("Deregistering RPC handlers for shutdown...");
|
||||
garage.system.netapp.drop_all_handlers();
|
||||
@@ -168,6 +176,12 @@ pub async fn run_server(
|
||||
// Await for all background tasks to end
|
||||
await_background_done.await?;
|
||||
|
||||
// Await for Consul deregistration to end, if it hasn't already
|
||||
#[cfg(feature = "consul-discovery")]
|
||||
if let Err(e) = deregister_consul_task.await {
|
||||
error!("Error while joining Consul deregistration task: {}", e);
|
||||
}
|
||||
|
||||
info!("Cleaning up...");
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -2,6 +2,7 @@ mod cors;
|
||||
mod list;
|
||||
mod multipart;
|
||||
mod objects;
|
||||
mod postobject;
|
||||
mod presigned;
|
||||
mod signature_encoding;
|
||||
mod simple;
|
||||
|
||||
@@ -554,7 +554,7 @@ async fn test_deleteobject() {
|
||||
|
||||
assert!(l.contents.is_none());
|
||||
|
||||
// Deleting a non-existing object shouldn't be a problem
|
||||
// Deleting a non-existing object shouldn't be a problem...
|
||||
ctx.client
|
||||
.delete_object()
|
||||
.bucket(&bucket)
|
||||
@@ -562,4 +562,27 @@ async fn test_deleteobject() {
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// ...and bulk-deleting a non-existing object shouldn't be either
|
||||
let r = ctx
|
||||
.client
|
||||
.delete_objects()
|
||||
.bucket(&bucket)
|
||||
.delete(
|
||||
Delete::builder()
|
||||
.objects(
|
||||
ObjectIdentifier::builder()
|
||||
.key("does-not-exist")
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(r.deleted.unwrap().len(), 1); // reported as deleted...
|
||||
assert!(r.errors.unwrap_or_default().is_empty()); // ...not as an error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
use base64::prelude::*;
|
||||
use bytes::Bytes;
|
||||
use chrono::{Duration, Utc};
|
||||
use hmac::Mac;
|
||||
use http_body_util::Full;
|
||||
use hyper::body::Incoming;
|
||||
use hyper::{header, Method, Request, Response, StatusCode};
|
||||
|
||||
use garage_api_common::signature;
|
||||
|
||||
use crate::common;
|
||||
|
||||
const UTF8_KEY: &str = "uploads/test/копия файла.jpg";
|
||||
const UTF8_FILENAME: &str = "café-日本語.txt";
|
||||
const UTF8_CONTENT_DISPOSITION: &str = "attachment; filename=\"копия файла.jpg\"";
|
||||
const REGION: &str = "garage-integ-test";
|
||||
const BOUNDARY: &str = "boundary-garage-integ-test";
|
||||
|
||||
async fn send_post_object(
|
||||
ctx: &common::Context,
|
||||
bucket: &str,
|
||||
key_field: &str,
|
||||
filename: &str,
|
||||
extra_fields: &[(&str, &str)],
|
||||
file_body: &str,
|
||||
) -> Response<Incoming> {
|
||||
let now = Utc::now();
|
||||
let scope = signature::compute_scope(&now, REGION, "s3");
|
||||
let credential = format!("{}/{}", ctx.key.id, scope);
|
||||
let date = now.format(signature::LONG_DATETIME).to_string();
|
||||
let expiration = (now + Duration::hours(1)).to_rfc3339();
|
||||
|
||||
let mut conditions = vec![
|
||||
serde_json::json!({ "bucket": bucket }),
|
||||
serde_json::json!(["starts-with", "$key", ""]),
|
||||
serde_json::json!({ "x-amz-algorithm": "AWS4-HMAC-SHA256" }),
|
||||
serde_json::json!({ "x-amz-credential": &credential }),
|
||||
serde_json::json!({ "x-amz-date": &date }),
|
||||
];
|
||||
for (name, value) in extra_fields {
|
||||
conditions.push(serde_json::json!(["eq", format!("${}", name), value]));
|
||||
}
|
||||
let policy = serde_json::json!({
|
||||
"expiration": expiration,
|
||||
"conditions": conditions,
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let policy_b64 = BASE64_STANDARD.encode(policy.as_bytes());
|
||||
|
||||
let mut signer = signature::signing_hmac(&now, &ctx.key.secret, REGION, "s3").unwrap();
|
||||
signer.update(policy_b64.as_bytes());
|
||||
let x_amz_signature = hex::encode(signer.finalize().into_bytes());
|
||||
|
||||
let mut fields = vec![
|
||||
("key".to_string(), key_field.to_string()),
|
||||
("x-amz-algorithm".into(), "AWS4-HMAC-SHA256".into()),
|
||||
("x-amz-credential".into(), credential),
|
||||
("x-amz-date".into(), date),
|
||||
("policy".into(), policy_b64),
|
||||
("x-amz-signature".into(), x_amz_signature),
|
||||
];
|
||||
for (name, value) in extra_fields {
|
||||
fields.push((name.to_string(), value.to_string()));
|
||||
}
|
||||
|
||||
let mut body = String::new();
|
||||
for (name, value) in &fields {
|
||||
body.push_str(&format!(
|
||||
"--{BOUNDARY}\r\nContent-Disposition: form-data; name=\"{name}\"\r\n\r\n{value}\r\n"
|
||||
));
|
||||
}
|
||||
|
||||
body.push_str(&format!(
|
||||
"--{BOUNDARY}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{filename}\"\r\nContent-Type: text/plain\r\n\r\n{file_body}\r\n--{BOUNDARY}--\r\n"
|
||||
));
|
||||
|
||||
let req = Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri(format!("{}{}", ctx.garage.s3_uri(), bucket))
|
||||
.header(header::HOST, "s3.garage")
|
||||
.header(
|
||||
header::CONTENT_TYPE,
|
||||
format!("multipart/form-data; boundary={BOUNDARY}"),
|
||||
)
|
||||
.body(Full::new(Bytes::from(body.into_bytes())))
|
||||
.unwrap();
|
||||
|
||||
ctx.custom_request.client().request(req).await.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_post_object_utf8_key() {
|
||||
let ctx = common::context();
|
||||
let bucket = ctx.create_bucket("post-object-utf8-key");
|
||||
|
||||
let res = send_post_object(&ctx, &bucket, UTF8_KEY, "копия файла.jpg", &[], "hello").await;
|
||||
assert_eq!(res.status(), StatusCode::NO_CONTENT);
|
||||
|
||||
let obj = ctx
|
||||
.client
|
||||
.get_object()
|
||||
.bucket(&bucket)
|
||||
.key(UTF8_KEY)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_bytes_eq!(obj.body, b"hello");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_post_object_utf8_filename_substitution() {
|
||||
let ctx = common::context();
|
||||
let bucket = ctx.create_bucket("post-object-utf8-filename");
|
||||
|
||||
let res = send_post_object(
|
||||
&ctx,
|
||||
&bucket,
|
||||
"uploads/${filename}",
|
||||
UTF8_FILENAME,
|
||||
&[],
|
||||
"bonjour",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(res.status(), StatusCode::NO_CONTENT);
|
||||
|
||||
let obj = ctx
|
||||
.client
|
||||
.get_object()
|
||||
.bucket(&bucket)
|
||||
.key(format!("uploads/{}", UTF8_FILENAME))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_bytes_eq!(obj.body, b"bonjour");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_post_object_utf8_content_disposition_metadata() {
|
||||
let ctx = common::context();
|
||||
let bucket = ctx.create_bucket("post-object-utf8-contdisp");
|
||||
|
||||
let res = send_post_object(
|
||||
&ctx,
|
||||
&bucket,
|
||||
"ascii-key.jpg",
|
||||
"file.jpg",
|
||||
&[("content-disposition", UTF8_CONTENT_DISPOSITION)],
|
||||
"data",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(res.status(), StatusCode::NO_CONTENT);
|
||||
|
||||
let obj = ctx
|
||||
.client
|
||||
.get_object()
|
||||
.bucket(&bucket)
|
||||
.key("ascii-key.jpg")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
obj.content_disposition.as_deref(),
|
||||
Some(UTF8_CONTENT_DISPOSITION)
|
||||
);
|
||||
}
|
||||
@@ -24,7 +24,7 @@ percent-encoding.workspace = true
|
||||
hyper = { workspace = true, default-features = false, features = ["http1", "http2"] }
|
||||
hyper-util.workspace = true
|
||||
hyper-rustls.workspace = true
|
||||
serde.workspace = true
|
||||
serde = { workspace = true, default-features = false, features = ["derive", "std"] }
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -26,6 +26,10 @@ mod error;
|
||||
|
||||
pub use error::Error;
|
||||
|
||||
// Re-export these crates: values from these are passed to K2vClient::new_with_client
|
||||
pub use hyper_rustls;
|
||||
pub use hyper_util;
|
||||
|
||||
pub type Body = FullBody<Bytes>;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
@@ -46,6 +50,7 @@ const PATH_ENCODE_SET: AsciiSet = NON_ALPHANUMERIC
|
||||
.remove(b'.')
|
||||
.remove(b'~');
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct K2vClientConfig {
|
||||
pub endpoint: String,
|
||||
pub region: String,
|
||||
@@ -56,6 +61,7 @@ pub struct K2vClientConfig {
|
||||
}
|
||||
|
||||
/// Client used to query a K2V server.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct K2vClient {
|
||||
config: K2vClientConfig,
|
||||
user_agent: HeaderValue,
|
||||
|
||||
@@ -30,7 +30,6 @@ thiserror.workspace = true
|
||||
hex.workspace = true
|
||||
http.workspace = true
|
||||
base64.workspace = true
|
||||
parse_duration.workspace = true
|
||||
tracing.workspace = true
|
||||
rand.workspace = true
|
||||
zstd.workspace = true
|
||||
|
||||
@@ -8,6 +8,7 @@ use garage_table::{EmptyKey, Entry, TableSchema};
|
||||
pub use crate::key_table::KeyFilter;
|
||||
|
||||
mod v2 {
|
||||
use crate::permission::ExpirationTime;
|
||||
use garage_util::crdt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -35,7 +36,7 @@ mod v2 {
|
||||
pub name: crdt::Lww<String>,
|
||||
|
||||
/// The optional time of expiration of the token
|
||||
pub expiration: crdt::Lww<Option<u64>>,
|
||||
pub expiration: crdt::Lww<crdt::MergingOption<ExpirationTime>>,
|
||||
|
||||
/// The scope of the token, i.e. list of authorized admin API calls
|
||||
pub scope: crdt::Lww<AdminApiTokenScope>,
|
||||
@@ -106,7 +107,7 @@ impl AdminApiToken {
|
||||
created: now_msec(),
|
||||
token_hash: hashed_token,
|
||||
name: crdt::Lww::new(name.to_string()),
|
||||
expiration: crdt::Lww::new(None),
|
||||
expiration: crdt::Lww::new(None.into()),
|
||||
scope: crdt::Lww::new(AdminApiTokenScope(vec!["*".to_string()])),
|
||||
}),
|
||||
};
|
||||
@@ -147,9 +148,9 @@ impl AdminApiToken {
|
||||
|
||||
impl AdminApiTokenParams {
|
||||
pub fn is_expired(&self, ts_now: u64) -> bool {
|
||||
match *self.expiration.get() {
|
||||
match self.expiration.get().inner() {
|
||||
None => false,
|
||||
Some(exp) => ts_now >= exp,
|
||||
Some(exp) => ts_now >= exp.0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ mod v08 {
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct BucketAlias {
|
||||
pub(super) name: String,
|
||||
pub state: crdt::Lww<Option<Uuid>>,
|
||||
pub state: crdt::Lww<crdt::CancelingOption<Uuid>>,
|
||||
}
|
||||
|
||||
impl garage_util::migrate::InitialFormat for BucketAlias {}
|
||||
@@ -25,12 +25,12 @@ impl BucketAlias {
|
||||
pub fn new(name: String, ts: u64, bucket_id: Option<Uuid>) -> Self {
|
||||
BucketAlias {
|
||||
name,
|
||||
state: crdt::Lww::raw(ts, bucket_id),
|
||||
state: crdt::Lww::raw(ts, CancelingOption(bucket_id)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_deleted(&self) -> bool {
|
||||
self.state.get().is_none()
|
||||
self.state.get().inner().is_none()
|
||||
}
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
|
||||
@@ -45,12 +45,12 @@ mod v08 {
|
||||
/// Whether this bucket is allowed for website access
|
||||
/// (under all of its global alias names),
|
||||
/// and if so, the website configuration XML document
|
||||
pub website_config: crdt::Lww<Option<WebsiteConfig>>,
|
||||
pub website_config: crdt::Lww<crdt::CancelingOption<WebsiteConfig>>,
|
||||
/// CORS rules
|
||||
pub cors_config: crdt::Lww<Option<Vec<CorsRule>>>,
|
||||
pub cors_config: crdt::Lww<crdt::CancelingOption<Vec<CorsRule>>>,
|
||||
/// Lifecycle configuration
|
||||
#[serde(default)]
|
||||
pub lifecycle_config: crdt::Lww<Option<Vec<LifecycleRule>>>,
|
||||
pub lifecycle_config: crdt::Lww<crdt::CancelingOption<Vec<LifecycleRule>>>,
|
||||
/// Bucket quotas
|
||||
#[serde(default)]
|
||||
pub quotas: crdt::Lww<BucketQuotas>,
|
||||
@@ -164,11 +164,11 @@ mod v2 {
|
||||
/// Whether this bucket is allowed for website access
|
||||
/// (under all of its global alias names),
|
||||
/// and if so, the website configuration XML document
|
||||
pub website_config: crdt::Lww<Option<WebsiteConfig>>,
|
||||
pub website_config: crdt::Lww<crdt::CancelingOption<WebsiteConfig>>,
|
||||
/// CORS rules
|
||||
pub cors_config: crdt::Lww<Option<Vec<CorsRule>>>,
|
||||
pub cors_config: crdt::Lww<crdt::CancelingOption<Vec<CorsRule>>>,
|
||||
/// Lifecycle configuration
|
||||
pub lifecycle_config: crdt::Lww<Option<Vec<LifecycleRule>>>,
|
||||
pub lifecycle_config: crdt::Lww<crdt::CancelingOption<Vec<LifecycleRule>>>,
|
||||
/// Bucket quotas
|
||||
pub quotas: crdt::Lww<BucketQuotas>,
|
||||
}
|
||||
@@ -259,9 +259,9 @@ impl BucketParams {
|
||||
authorized_keys: crdt::Map::new(),
|
||||
aliases: crdt::LwwMap::new(),
|
||||
local_aliases: crdt::LwwMap::new(),
|
||||
website_config: crdt::Lww::new(None),
|
||||
cors_config: crdt::Lww::new(None),
|
||||
lifecycle_config: crdt::Lww::new(None),
|
||||
website_config: crdt::Lww::new(None.into()),
|
||||
cors_config: crdt::Lww::new(None.into()),
|
||||
lifecycle_config: crdt::Lww::new(None.into()),
|
||||
quotas: crdt::Lww::new(BucketQuotas::default()),
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -304,7 +304,7 @@ impl Garage {
|
||||
self.k2v.spawn_workers(bg);
|
||||
|
||||
if let Some(itv) = self.config.metadata_auto_snapshot_interval.as_deref() {
|
||||
let interval = parse_duration::parse(itv)
|
||||
let interval = garage_util::time::parse_duration(itv)
|
||||
.ok_or_message("Invalid `metadata_auto_snapshot_interval`")?;
|
||||
if interval < std::time::Duration::from_secs(600) {
|
||||
return Err(Error::Message(
|
||||
|
||||
+15
-13
@@ -52,7 +52,7 @@ impl<'a> BucketHelper<'a> {
|
||||
.0
|
||||
.bucket_alias_table
|
||||
.get_local(&EmptyKey, bucket_name)?
|
||||
.and_then(|x| *x.state.get());
|
||||
.and_then(|x| x.state.get().into_inner());
|
||||
match alias {
|
||||
Some(id) => id,
|
||||
None => return Ok(None),
|
||||
@@ -91,15 +91,18 @@ impl<'a> BucketHelper<'a> {
|
||||
.as_option()
|
||||
.ok_or_message("Key should not be deleted at this point")?;
|
||||
|
||||
let bucket_opt =
|
||||
if let Some(Some(bucket_id)) = api_key_params.local_aliases.get(bucket_name) {
|
||||
self.0
|
||||
.bucket_table
|
||||
.get_local(&EmptyKey, bucket_id)?
|
||||
.filter(|x| !x.state.is_deleted())
|
||||
} else {
|
||||
self.resolve_global_bucket_fast(bucket_name)?
|
||||
};
|
||||
let bucket_opt = if let Some(bucket_id) = api_key_params
|
||||
.local_aliases
|
||||
.get(bucket_name)
|
||||
.and_then(|x| x.inner())
|
||||
{
|
||||
self.0
|
||||
.bucket_table
|
||||
.get_local(&EmptyKey, bucket_id)?
|
||||
.filter(|x| !x.state.is_deleted())
|
||||
} else {
|
||||
self.resolve_global_bucket_fast(bucket_name)?
|
||||
};
|
||||
bucket_opt.ok_or_else(|| Error::NoSuchBucket(bucket_name.to_string()))
|
||||
}
|
||||
|
||||
@@ -125,7 +128,7 @@ impl<'a> BucketHelper<'a> {
|
||||
.bucket_alias_table
|
||||
.get(&EmptyKey, bucket_name)
|
||||
.await?
|
||||
.and_then(|x| *x.state.get());
|
||||
.and_then(|x| x.state.get().into_inner());
|
||||
match alias {
|
||||
Some(id) => id,
|
||||
None => return Ok(None),
|
||||
@@ -163,8 +166,7 @@ impl<'a> BucketHelper<'a> {
|
||||
.ok_or_else(|| GarageError::Message(format!("access key {} has been deleted", key_id)))?
|
||||
.local_aliases
|
||||
.get(bucket_name)
|
||||
.copied()
|
||||
.flatten();
|
||||
.and_then(|x| x.inner().copied());
|
||||
|
||||
if let Some(bucket_id) = local_alias {
|
||||
Ok(self
|
||||
|
||||
+34
-18
@@ -74,8 +74,8 @@ impl<'a> LockedHelper<'a> {
|
||||
let alias = self.0.bucket_alias_table.get(&EmptyKey, alias_name).await?;
|
||||
|
||||
if let Some(existing_alias) = alias.as_ref() {
|
||||
if let Some(p_bucket) = existing_alias.state.get() {
|
||||
if *p_bucket != bucket_id {
|
||||
if let Some(p_bucket) = existing_alias.state.get().into_inner() {
|
||||
if p_bucket != bucket_id {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"Alias {} already exists and points to different bucket: {:?}",
|
||||
alias_name, p_bucket
|
||||
@@ -98,7 +98,7 @@ impl<'a> LockedHelper<'a> {
|
||||
let alias = match alias {
|
||||
None => BucketAlias::new(alias_name.clone(), alias_ts, Some(bucket_id)),
|
||||
Some(mut a) => {
|
||||
a.state = Lww::raw(alias_ts, Some(bucket_id));
|
||||
a.state = Lww::raw(alias_ts, Some(bucket_id).into());
|
||||
a
|
||||
}
|
||||
};
|
||||
@@ -128,7 +128,13 @@ impl<'a> LockedHelper<'a> {
|
||||
.bucket_alias_table
|
||||
.get(&EmptyKey, alias_name)
|
||||
.await?
|
||||
.filter(|a| a.state.get().map(|x| x == bucket_id).unwrap_or(false))
|
||||
.filter(|a| {
|
||||
a.state
|
||||
.get()
|
||||
.into_inner()
|
||||
.map(|x| x == bucket_id)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.ok_or_message(format!(
|
||||
"Internal error: alias not found or does not point to bucket {:?}",
|
||||
bucket_id
|
||||
@@ -157,7 +163,7 @@ impl<'a> LockedHelper<'a> {
|
||||
// ---- timestamp-ensured causality barrier ----
|
||||
// writes are now done and all writes use timestamp alias_ts
|
||||
|
||||
alias.state = Lww::raw(alias_ts, None);
|
||||
alias.state = Lww::raw(alias_ts, None.into());
|
||||
self.0.bucket_alias_table.insert(&alias).await?;
|
||||
|
||||
bucket_state.aliases = LwwMap::raw_item(alias_name.clone(), alias_ts, false);
|
||||
@@ -199,8 +205,8 @@ impl<'a> LockedHelper<'a> {
|
||||
// ---- timestamp-ensured causality barrier ----
|
||||
// writes are now done and all writes use timestamp alias_ts
|
||||
|
||||
if alias.state.get() == &Some(bucket_id) {
|
||||
alias.state = Lww::raw(alias_ts, None);
|
||||
if alias.state.get().inner() == Some(&bucket_id) {
|
||||
alias.state = Lww::raw(alias_ts, None.into());
|
||||
self.0.bucket_alias_table.insert(&alias).await?;
|
||||
}
|
||||
|
||||
@@ -237,7 +243,11 @@ impl<'a> LockedHelper<'a> {
|
||||
|
||||
let key_param = key.state.as_option_mut().unwrap();
|
||||
|
||||
if let Some(Some(existing_alias)) = key_param.local_aliases.get(alias_name) {
|
||||
if let Some(Some(existing_alias)) = key_param
|
||||
.local_aliases
|
||||
.get(alias_name)
|
||||
.map(CancelingOption::inner)
|
||||
{
|
||||
if *existing_alias != bucket_id {
|
||||
return Err(Error::BadRequest(format!("Alias {} already exists in namespace of key {} and points to different bucket: {:?}", alias_name, key.key_id, existing_alias)));
|
||||
}
|
||||
@@ -261,7 +271,8 @@ impl<'a> LockedHelper<'a> {
|
||||
// ---- timestamp-ensured causality barrier ----
|
||||
// writes are now done and all writes use timestamp alias_ts
|
||||
|
||||
key_param.local_aliases = LwwMap::raw_item(alias_name.clone(), alias_ts, Some(bucket_id));
|
||||
key_param.local_aliases =
|
||||
LwwMap::raw_item(alias_name.clone(), alias_ts, Some(bucket_id).into());
|
||||
self.0.key_table.insert(&key).await?;
|
||||
|
||||
bucket_p.local_aliases = LwwMap::raw_item(bucket_p_local_alias_key, alias_ts, true);
|
||||
@@ -288,7 +299,12 @@ impl<'a> LockedHelper<'a> {
|
||||
let key_p = key.state.as_option().unwrap();
|
||||
let bucket_p = bucket.state.as_option_mut().unwrap();
|
||||
|
||||
if key_p.local_aliases.get(alias_name).cloned().flatten() != Some(bucket_id) {
|
||||
if key_p
|
||||
.local_aliases
|
||||
.get(alias_name)
|
||||
.and_then(CancelingOption::inner)
|
||||
!= Some(&bucket_id)
|
||||
{
|
||||
return Err(GarageError::Message(format!(
|
||||
"Bucket {:?} does not have alias {} in namespace of key {}",
|
||||
bucket_id, alias_name, key_id
|
||||
@@ -325,7 +341,7 @@ impl<'a> LockedHelper<'a> {
|
||||
// writes are now done and all writes use timestamp alias_ts
|
||||
|
||||
key.state.as_option_mut().unwrap().local_aliases =
|
||||
LwwMap::raw_item(alias_name.clone(), alias_ts, None);
|
||||
LwwMap::raw_item(alias_name.clone(), alias_ts, None.into());
|
||||
self.0.key_table.insert(&key).await?;
|
||||
|
||||
bucket_p.local_aliases = LwwMap::raw_item(bucket_p_local_alias_key, alias_ts, false);
|
||||
@@ -367,7 +383,7 @@ impl<'a> LockedHelper<'a> {
|
||||
// writes are now done and all writes use timestamp alias_ts
|
||||
|
||||
if let Some(kp) = key.state.as_option_mut() {
|
||||
kp.local_aliases = LwwMap::raw_item(alias_name.clone(), alias_ts, None);
|
||||
kp.local_aliases = LwwMap::raw_item(alias_name.clone(), alias_ts, None.into());
|
||||
self.0.key_table.insert(&key).await?;
|
||||
}
|
||||
|
||||
@@ -444,8 +460,8 @@ impl<'a> LockedHelper<'a> {
|
||||
|
||||
// 1. Delete local aliases
|
||||
for (alias, _, to) in state.local_aliases.items().iter() {
|
||||
if let Some(bucket_id) = to {
|
||||
self.purge_local_bucket_alias(*bucket_id, &key.key_id, alias)
|
||||
if let Some(bucket_id) = to.into_inner() {
|
||||
self.purge_local_bucket_alias(bucket_id, &key.key_id, alias)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
@@ -501,7 +517,7 @@ impl<'a> LockedHelper<'a> {
|
||||
.data
|
||||
.decode_entry(&(item?.1))
|
||||
.map_err(db::TxError::Abort)?;
|
||||
if let Some(id) = alias.state.get() {
|
||||
if let Some(id) = alias.state.get().inner() {
|
||||
if all_buckets.contains(id) {
|
||||
// keep aliases
|
||||
global_aliases.insert(alias.name().to_string(), *id);
|
||||
@@ -512,7 +528,7 @@ impl<'a> LockedHelper<'a> {
|
||||
alias.name(),
|
||||
id
|
||||
);
|
||||
alias.state.update(None);
|
||||
alias.state.update(None.into());
|
||||
delete_global.push(alias);
|
||||
}
|
||||
}
|
||||
@@ -544,7 +560,7 @@ impl<'a> LockedHelper<'a> {
|
||||
};
|
||||
let mut has_changes = false;
|
||||
for (name, _, to) in p.local_aliases.items().to_vec() {
|
||||
if let Some(id) = to {
|
||||
if let Some(id) = to.into_inner() {
|
||||
if all_buckets.contains(&id) {
|
||||
local_aliases.insert((key.key_id.clone(), name), id);
|
||||
} else {
|
||||
@@ -552,7 +568,7 @@ impl<'a> LockedHelper<'a> {
|
||||
"local alias: remove ({}, {}) -> {:?} (bucket is deleted)",
|
||||
key.key_id, name, id
|
||||
);
|
||||
p.local_aliases.update_in_place(name, None);
|
||||
p.local_aliases.update_in_place(name, None.into());
|
||||
has_changes = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +163,7 @@ impl<T: CountedItem> TableSchema for CounterTable<T> {
|
||||
|
||||
pub struct IndexCounter<T: CountedItem> {
|
||||
this_node: Uuid,
|
||||
// TODO: migrate to TypedTree
|
||||
local_counter: db::Tree,
|
||||
pub table: Arc<Table<CounterTable<T>, TableShardedReplication>>,
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ mod v08 {
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
||||
pub enum DvvsValue {
|
||||
Value(#[serde(with = "serde_bytes")] Vec<u8>),
|
||||
Deleted,
|
||||
@@ -131,9 +132,26 @@ impl K2VItem {
|
||||
ent.discard();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_raw_items(items: BTreeMap<K2VNodeId, DvvsEntry>) -> Self {
|
||||
let mut item = K2VItem {
|
||||
partition: K2VItemPartition {
|
||||
bucket_id: [0u8; 32].into(),
|
||||
partition_key: String::new(),
|
||||
},
|
||||
sort_key: String::new(),
|
||||
items,
|
||||
};
|
||||
item.discard();
|
||||
item
|
||||
}
|
||||
}
|
||||
|
||||
impl DvvsEntry {
|
||||
pub fn from_raw(t_discard: u64, values: Vec<(u64, DvvsValue)>) -> Self {
|
||||
DvvsEntry { t_discard, values }
|
||||
}
|
||||
|
||||
fn max_time(&self) -> u64 {
|
||||
self.values
|
||||
.iter()
|
||||
@@ -162,15 +180,26 @@ impl Crdt for K2VItem {
|
||||
|
||||
impl Crdt for DvvsEntry {
|
||||
fn merge(&mut self, other: &Self) {
|
||||
self.t_discard = std::cmp::max(self.t_discard, other.t_discard);
|
||||
self.discard();
|
||||
|
||||
let t_max = self.max_time();
|
||||
for (vt, vv) in other.values.iter() {
|
||||
if *vt > t_max {
|
||||
self.values.push((*vt, vv.clone()));
|
||||
let mut slf = std::mem::take(&mut self.values).into_iter().peekable();
|
||||
let mut otr = other.values.iter().peekable();
|
||||
while let (Some((slf_t, _)), Some((otr_t, _))) = (slf.peek(), otr.peek()) {
|
||||
match slf_t.cmp(otr_t) {
|
||||
std::cmp::Ordering::Less => {
|
||||
self.values.push(slf.next().unwrap());
|
||||
}
|
||||
std::cmp::Ordering::Equal => {
|
||||
self.values.push(slf.next().unwrap());
|
||||
otr.next();
|
||||
}
|
||||
std::cmp::Ordering::Greater => {
|
||||
self.values.push(otr.next().unwrap().clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
self.values.extend(slf);
|
||||
self.values.extend(otr.cloned());
|
||||
self.t_discard = std::cmp::max(self.t_discard, other.t_discard);
|
||||
self.discard();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+72
-16
@@ -5,7 +5,7 @@
|
||||
//! node does not process the entry directly, as this would
|
||||
//! mean the vector clock gets much larger than needed).
|
||||
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::convert::TryInto;
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -38,6 +38,12 @@ const POLL_RANGE_EXTRA_DELAY: Duration = Duration::from_millis(200);
|
||||
|
||||
const TIMESTAMP_KEY: &[u8] = b"timestamp";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum K2VMonotonicRead {
|
||||
Monotonic,
|
||||
NonMonotonic,
|
||||
}
|
||||
|
||||
/// RPC messages for K2V
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
enum K2VRpc {
|
||||
@@ -78,6 +84,7 @@ pub struct K2VRpcHandler {
|
||||
// Using a mutex on the local_timestamp_tree is not strictly necessary,
|
||||
// but it helps to not try to do several inserts at the same time,
|
||||
// which would create transaction conflicts and force many useless retries.
|
||||
// TODO: migrate to TypedTree
|
||||
local_timestamp_tree: Mutex<db::Tree>,
|
||||
|
||||
endpoint: Arc<Endpoint<K2VRpc, Self>>,
|
||||
@@ -210,6 +217,7 @@ impl K2VRpcHandler {
|
||||
sort_key: String,
|
||||
causal_context: CausalContext,
|
||||
timeout_msec: u64,
|
||||
monotonic_read: K2VMonotonicRead,
|
||||
) -> Result<Option<K2VItem>, Error> {
|
||||
let poll_key = PollKey {
|
||||
partition: K2VItemPartition {
|
||||
@@ -244,17 +252,35 @@ impl K2VRpcHandler {
|
||||
};
|
||||
|
||||
let mut resp: Option<K2VItem> = None;
|
||||
for v in resps {
|
||||
match v {
|
||||
K2VRpc::PollItemResponse(Some(x)) => {
|
||||
if let Some(y) = &mut resp {
|
||||
y.merge(&x);
|
||||
} else {
|
||||
resp = Some(x);
|
||||
let mut not_all_same = false;
|
||||
{
|
||||
let mut vals_nb = 0;
|
||||
let resps_nb = resps.len();
|
||||
for v in resps {
|
||||
match v {
|
||||
K2VRpc::PollItemResponse(Some(x)) => {
|
||||
vals_nb += 1;
|
||||
if let Some(y) = &mut resp {
|
||||
if *y != x {
|
||||
not_all_same = true;
|
||||
y.merge(&x);
|
||||
}
|
||||
} else {
|
||||
resp = Some(x);
|
||||
}
|
||||
}
|
||||
K2VRpc::PollItemResponse(None) => (),
|
||||
v => return Err(Error::unexpected_rpc_message(v)),
|
||||
}
|
||||
K2VRpc::PollItemResponse(None) => (),
|
||||
v => return Err(Error::unexpected_rpc_message(v)),
|
||||
}
|
||||
if vals_nb < resps_nb {
|
||||
not_all_same = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(v) = &resp {
|
||||
if monotonic_read == K2VMonotonicRead::Monotonic && not_all_same {
|
||||
self.item_table.repair_on_read(&nodes, &[v]).await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,6 +292,7 @@ impl K2VRpcHandler {
|
||||
range: PollRange,
|
||||
seen_str: Option<String>,
|
||||
timeout_msec: u64,
|
||||
monotonic_read: K2VMonotonicRead,
|
||||
) -> Result<Option<(BTreeMap<String, K2VItem>, String)>, HelperError> {
|
||||
let has_seen_marker = seen_str.is_some();
|
||||
|
||||
@@ -343,24 +370,53 @@ impl K2VRpcHandler {
|
||||
|
||||
// Take all returned items into account to produce the response.
|
||||
let mut new_items = BTreeMap::<String, K2VItem>::new();
|
||||
for v in resps {
|
||||
if let K2VRpc::PollRangeResponse(node, items) = v {
|
||||
seen.mark_seen_node_items(node, items.iter());
|
||||
let mut to_repair = BTreeSet::new();
|
||||
{
|
||||
let mut all_items: BTreeMap<_, Vec<_>> = BTreeMap::new();
|
||||
let resps_nb = resps.len();
|
||||
for v in resps {
|
||||
if let K2VRpc::PollRangeResponse(node, items) = v {
|
||||
seen.mark_seen_node_items(node, items.iter());
|
||||
for item in items.into_iter() {
|
||||
all_items
|
||||
.entry(item.sort_key.clone())
|
||||
.or_default()
|
||||
.push(item);
|
||||
}
|
||||
} else {
|
||||
return Err(Error::unexpected_rpc_message(v).into());
|
||||
}
|
||||
}
|
||||
for (item_key, items) in all_items {
|
||||
// Only some nodes store this item; we must propagate it during repair
|
||||
if items.len() < resps_nb {
|
||||
to_repair.insert(item_key.clone());
|
||||
}
|
||||
// Merge all items for this key together
|
||||
for item in items.into_iter() {
|
||||
match new_items.get_mut(&item.sort_key) {
|
||||
Some(ent) => {
|
||||
ent.merge(&item);
|
||||
if *ent != item {
|
||||
ent.merge(&item);
|
||||
to_repair.insert(item.sort_key.clone());
|
||||
}
|
||||
}
|
||||
None => {
|
||||
new_items.insert(item.sort_key.clone(), item);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(Error::unexpected_rpc_message(v).into());
|
||||
}
|
||||
}
|
||||
|
||||
if monotonic_read == K2VMonotonicRead::Monotonic && !to_repair.is_empty() {
|
||||
let to_repair: Vec<_> = to_repair
|
||||
.into_iter()
|
||||
.map(|k| new_items.get(&k).unwrap())
|
||||
.collect();
|
||||
self.item_table.repair_on_read(&nodes, &to_repair).await?;
|
||||
}
|
||||
|
||||
if new_items.is_empty() && has_seen_marker {
|
||||
Ok(None)
|
||||
} else {
|
||||
|
||||
@@ -43,7 +43,7 @@ mod v08 {
|
||||
|
||||
/// A key can have a local view of buckets names it is
|
||||
/// the only one to see, this is the namespace for these aliases
|
||||
pub local_aliases: crdt::LwwMap<String, Option<Uuid>>,
|
||||
pub local_aliases: crdt::LwwMap<String, crdt::CancelingOption<Uuid>>,
|
||||
}
|
||||
|
||||
impl garage_util::migrate::InitialFormat for Key {}
|
||||
@@ -51,6 +51,7 @@ mod v08 {
|
||||
|
||||
mod v2 {
|
||||
use crate::permission::BucketKeyPerm;
|
||||
use crate::permission::ExpirationTime;
|
||||
use garage_util::crdt;
|
||||
use garage_util::data::Uuid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -79,7 +80,7 @@ mod v2 {
|
||||
/// Name for the key
|
||||
pub name: crdt::Lww<String>,
|
||||
/// The optional time of expiration of the key
|
||||
pub expiration: crdt::Lww<Option<u64>>,
|
||||
pub expiration: crdt::Lww<crdt::MergingOption<ExpirationTime>>,
|
||||
|
||||
/// Flag to allow users having this key to create buckets
|
||||
pub allow_create_bucket: crdt::Lww<bool>,
|
||||
@@ -91,7 +92,7 @@ mod v2 {
|
||||
|
||||
/// A key can have a local view of buckets names it is
|
||||
/// the only one to see, this is the namespace for these aliases
|
||||
pub local_aliases: crdt::LwwMap<String, Option<Uuid>>,
|
||||
pub local_aliases: crdt::LwwMap<String, crdt::CancelingOption<Uuid>>,
|
||||
}
|
||||
|
||||
impl garage_util::migrate::Migrate for Key {
|
||||
@@ -106,7 +107,7 @@ mod v2 {
|
||||
created: None,
|
||||
secret_key: x.secret_key,
|
||||
name: x.name,
|
||||
expiration: crdt::Lww::raw(0, None),
|
||||
expiration: crdt::Lww::raw(0, None.into()),
|
||||
allow_create_bucket: x.allow_create_bucket,
|
||||
authorized_buckets: x.authorized_buckets,
|
||||
local_aliases: x.local_aliases,
|
||||
@@ -124,7 +125,7 @@ impl KeyParams {
|
||||
created: Some(now_msec()),
|
||||
secret_key: secret_key.to_string(),
|
||||
name: crdt::Lww::new(name.to_string()),
|
||||
expiration: crdt::Lww::new(None),
|
||||
expiration: crdt::Lww::new(None.into()),
|
||||
allow_create_bucket: crdt::Lww::new(false),
|
||||
authorized_buckets: crdt::Map::new(),
|
||||
local_aliases: crdt::LwwMap::new(),
|
||||
@@ -229,9 +230,9 @@ impl Key {
|
||||
|
||||
impl KeyParams {
|
||||
pub fn is_expired(&self, ts_now: u64) -> bool {
|
||||
match *self.expiration.get() {
|
||||
match self.expiration.get().inner() {
|
||||
None => false,
|
||||
Some(exp) => ts_now >= exp,
|
||||
Some(exp) => ts_now >= exp.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,3 +63,15 @@ impl Crdt for BucketKeyPerm {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Expiration date for a key or token
|
||||
#[derive(PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
||||
#[serde(transparent)]
|
||||
pub struct ExpirationTime(pub u64);
|
||||
|
||||
impl Crdt for ExpirationTime {
|
||||
fn merge(&mut self, other: &Self) {
|
||||
self.0 = std::cmp::min(self.0, other.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +271,7 @@ async fn process_object(
|
||||
let lifecycle_policy: &[LifecycleRule] = bucket
|
||||
.state
|
||||
.as_option()
|
||||
.and_then(|s| s.lifecycle_config.get().as_deref())
|
||||
.and_then(|s| s.lifecycle_config.get().inner().map(|x| &x[..]))
|
||||
.unwrap_or_default();
|
||||
|
||||
if lifecycle_policy.iter().all(|x| !x.enabled) {
|
||||
|
||||
@@ -43,6 +43,7 @@ pub(crate) const NETAPP_VERSION_TAG: u64 = 0x6772676e65740010; // grgnet 0x0010
|
||||
/// Time a connection must be idle before the first keepalive probe is sent.
|
||||
const TCP_KEEPALIVE_TIME: Duration = Duration::from_secs(30);
|
||||
/// Interval between keepalive probes after the first.
|
||||
#[cfg(not(target_os = "openbsd"))]
|
||||
const TCP_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(10);
|
||||
|
||||
/// Timeout for outgoing TCP connection attempts.
|
||||
@@ -52,9 +53,15 @@ const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
fn set_keepalive(stream: &TcpStream) -> Result<(), std::io::Error> {
|
||||
let sock_ref = socket2::SockRef::from(stream);
|
||||
// OpenBSD does not support with_interval method
|
||||
#[cfg(not(target_os = "openbsd"))]
|
||||
let keepalive = socket2::TcpKeepalive::new()
|
||||
.with_time(TCP_KEEPALIVE_TIME)
|
||||
.with_interval(TCP_KEEPALIVE_INTERVAL);
|
||||
|
||||
#[cfg(target_os = "openbsd")]
|
||||
let keepalive = socket2::TcpKeepalive::new().with_time(TCP_KEEPALIVE_TIME);
|
||||
|
||||
sock_ref.set_tcp_keepalive(&keepalive)
|
||||
}
|
||||
|
||||
|
||||
@@ -183,6 +183,36 @@ impl ConsulDiscovery {
|
||||
}
|
||||
// ---- PUBLISHING TO CONSUL CATALOG ----
|
||||
|
||||
#[cfg(feature = "consul-discovery")]
|
||||
pub async fn deregister_consul_service(&self, node_id: NodeID) -> Result<(), ConsulError> {
|
||||
let node = format!("garage:{}", hex::encode(&node_id[..8]));
|
||||
let url = format!(
|
||||
"{}/v1/{}",
|
||||
self.config.consul_http_addr,
|
||||
(match &self.config.api {
|
||||
ConsulDiscoveryAPI::Catalog => format!("catalog/deregister"),
|
||||
ConsulDiscoveryAPI::Agent => format!("agent/service/deregister/{}", node),
|
||||
})
|
||||
);
|
||||
|
||||
let req = self.client.put(&url);
|
||||
|
||||
let http = if matches!(&self.config.api, ConsulDiscoveryAPI::Catalog) {
|
||||
let deregister_request = serde_json::json!({
|
||||
"Node": node,
|
||||
"ServiceID": node,
|
||||
});
|
||||
let req = req.json(&deregister_request);
|
||||
req.send().await?
|
||||
} else {
|
||||
req.send().await?
|
||||
};
|
||||
http.error_for_status()?;
|
||||
|
||||
debug!("Deregistered service {} from Consul", node);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn publish_consul_service(
|
||||
&self,
|
||||
node_id: NodeID,
|
||||
|
||||
+33
-6
@@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, RwLock, RwLockReadGuard};
|
||||
use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use arc_swap::ArcSwapOption;
|
||||
@@ -24,6 +24,7 @@ use garage_util::config::KubernetesDiscoveryConfig;
|
||||
use garage_util::config::{Config, DataDirEnum};
|
||||
use garage_util::data::*;
|
||||
use garage_util::error::*;
|
||||
use garage_util::migrate::Migrate;
|
||||
use garage_util::persister::Persister;
|
||||
use garage_util::time::*;
|
||||
|
||||
@@ -89,6 +90,9 @@ pub struct System {
|
||||
pub id: Uuid,
|
||||
|
||||
persist_peer_list: Persister<PeerList>,
|
||||
// Cache of the bytes last read from / written to the peer list file,
|
||||
// to avoid waking up the disk with a read on every save_peer_list() call.
|
||||
cached_peer_list_bytes: Mutex<Option<Vec<u8>>>,
|
||||
|
||||
pub(crate) local_status: RwLock<NodeStatus>,
|
||||
node_status: RwLock<HashMap<Uuid, (u64, NodeStatus)>>,
|
||||
@@ -311,6 +315,7 @@ impl System {
|
||||
let sys = Arc::new(System {
|
||||
id: netapp.id.into(),
|
||||
persist_peer_list,
|
||||
cached_peer_list_bytes: Mutex::new(None),
|
||||
local_status: RwLock::new(local_status),
|
||||
node_status: RwLock::new(HashMap::new()),
|
||||
netapp: netapp.clone(),
|
||||
@@ -353,7 +358,7 @@ impl System {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn cleanup(&self) {
|
||||
pub fn cleanup(self: &Arc<Self>) {
|
||||
// Break reference cycle
|
||||
self.metrics.store(None);
|
||||
}
|
||||
@@ -645,6 +650,15 @@ impl System {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "consul-discovery")]
|
||||
pub async fn deregister_from_discovery(self: &Arc<Self>) {
|
||||
if let Some(c) = &self.consul_discovery {
|
||||
if let Err(e) = c.deregister_consul_service(self.netapp.id).await {
|
||||
error!("Error while deregistering from Consul: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn discovery_loop(self: &Arc<Self>, mut stop_signal: watch::Receiver<bool>) {
|
||||
while !*stop_signal.borrow() {
|
||||
let peers_up = self
|
||||
@@ -779,10 +793,23 @@ impl System {
|
||||
peer_list.extend(prev_peer_list.0);
|
||||
}
|
||||
|
||||
// Save new peer list to file
|
||||
self.persist_peer_list
|
||||
.save_async(&PeerList(peer_list))
|
||||
.await
|
||||
// Sort for a deterministic encoding, so that we can detect below
|
||||
// whether the peer list actually changed since last time.
|
||||
peer_list.sort_by_key(|(id, _)| *id);
|
||||
|
||||
let new_peer_list = PeerList(peer_list);
|
||||
let new_peer_list_bytes = new_peer_list.encode()?;
|
||||
|
||||
// Skip writing to disk if nothing changed since the last save,
|
||||
// but still cache the bytes so we don't have to read the file again.
|
||||
let cached_peer_list_bytes = self.cached_peer_list_bytes.lock().unwrap().clone();
|
||||
if cached_peer_list_bytes.as_deref() == Some(&new_peer_list_bytes[..]) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.persist_peer_list.save_async(&new_peer_list).await?;
|
||||
*self.cached_peer_list_bytes.lock().unwrap() = Some(new_peer_list_bytes);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user