Compare commits

..

99 Commits

Author SHA1 Message Date
maximilien 19a29cce6d helm: fix test 2026-08-28 02:25:43 +02:00
maximilien d65be95051 helm: add unit test suite and complex valuefile example 2026-08-28 02:07:36 +02:00
maximilien ae2404027a helm: Ingress: only include yaml document delimiter when followed by content 2026-08-28 02:02:45 +02:00
maximilien 13d313134d helm: ServiceMonitor: validate tlsConfig and relabeling config before injecting it 2026-08-28 02:00:13 +02:00
maximilien 05e294307e Merge pull request 'doc: update on k2v inclusion in builds' (#1513) from vincent/garage:main-v2 into main-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1513
2026-08-23 19:57:51 +00:00
Vincent 109fbd49b1 Update information about k2v inclusion in builds 2026-08-23 20:57:21 +02:00
i5-650 d0176f8e30 deregister from consul on shutdown (fix #1062) (#1507)
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1507
2026-08-22 17:19:51 +00:00
i5-650 d9b1dba137 1250 - add NO_COLOR env var to disable logs colors (#1503)
Fix #1250

Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1503
Reviewed-by: Alex <lx@deuxfleurs.fr>
2026-08-19 08:03:44 +00:00
Agathe Porte c3c8af9a66 doc: quick-start: rework flag description (#1496)
Add a link to the manual install procedure, use a
quote to make the manual install path more visible
and mention the --default-access-key option.

Closes: #1446

Rendered output:

![image](/attachments/84bbe650-daff-40d1-b4bd-629abece27b0)

Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1496
2026-07-25 14:48:53 +00:00
Alex Auvolat ad78b9ee5c CI: revert spurrious changed introduced in #1487 that broke CI for main-v2 (#1499)
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1499
2026-07-25 13:25:17 +00:00
Alex b8e844612d Merge pull request 'Admin API: correctly return aliased buckets for keys even if they have no permissions' (#1498) from fix-aliases-no-perms into main-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1498
2026-07-25 12:38:23 +00:00
Alex Auvolat aa4e4656b1 GetKeyInfo: properly list locally-aliased buckets even if they have no permissions 2026-07-25 14:24:58 +02:00
Alex Auvolat 5549077d6f GetBucketInfo: properly return keys that have local aliases even if they have no permissions 2026-07-25 14:24:49 +02:00
smattymatty 663fc5ae48 fix(s3): allow UTF-8 in PostObject form field values (fix #1489) (#1492)
PostObject (presigned POST) returns `400 InvalidHeaderValue` when the upload involves a non-ASCII filename. The same key uploads fine vie PUT, as the issue #1489 noted.

The problem was that `handle_post_object` stores the form field values in an `http::HeaderMap`. values go in through `HeaderValue::from_str` but are read back with the strict `HeaderValue::to_str()` (ASCII-only) which fails on any UTF-8 value.

This is the same problem fixed in eab2b81b for `x-amz-meta-*` headers, and the fix is the same:
`std::str::from_utf8(value.as_bytes())` instead of `to_str()`. The `key` field in `post_object.rs` and the standard headers (`content-disposition` etc.) in `extract_metadata_headers`.

Added integration tests for PostObject (there were none): UTF-8 key, `${filename}` substitution, and UTF-8 `Content-Disposition` metadata. The substitution test passes even without the fix, proving that the filename path was never broken, only the form-params round-trip.

Co-authored-by: Mathew Storm <mathew@stormdevelopments.ca>
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1492
Reviewed-by: trinity-1686a <trinity-1686a@noreply.localhost>
2026-07-20 18:12:39 +00:00
Agathe Porte 0f89923d2d doc: websites: explain global aliases required (#1494)
Add a note explaining that a globally aliased bucket is
required for successfully exposing it as a website.

Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1494
2026-07-20 18:11:15 +00:00
Antonin Delpeuch a1f9c52563 docs: add GOVERNANCE.md (#1493)
Documenting the structure of a team can be helpful to integrate newcomers, avoid some conflicts and to accompany the evolution of that structure as a FOSS project grows.

This is an attempt to do that for garage, after discussions with multiple maintainers. The aim of this text is to document the status quo, offering a start for further incremental updates.
I propose to put it as `GOVERNANCE.md` file because this naming has emerged as a sort of standard in FOSS projects.

I would be happy to help facilitate such updates if there is interest (while being aware that project members have other things on their plates and that governance work can be taxing).

Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1493
2026-07-20 18:10:30 +00:00
Agathe Porte ceade99cd7 doc: fix dead link to WinSCP wiki page (#1495)
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1495
2026-07-20 18:08:59 +00:00
Agathe Porte d1333c1729 doc: multi-hdd: refer to rebalance op (#1491)
The rebalance op links to the multi-hdd page, but the multi-hdd page
does not refer to the rebalance op description and only states that an
operator can launch a repair procedure without stating which procedure.

Add a link to the rebalance repair procedure in the multi-hdd page to
make it easier to find what procedure to run for rebalance.

Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1491
2026-07-14 21:12:45 +00:00
Andreas Schneider 7e246b20e9 fix: avoid rewriting peer list file when unchanged (#1488)
save_peer_list() ran unconditionally every 60s from the discovery
loop, writing the same data to disk even when the peer set hadn't
changed. Compare the newly encoded peer list against the bytes
already on disk and skip the write when they match.

Fixes #1457

Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1488
Reviewed-by: Alex <lx@deuxfleurs.fr>
2026-07-14 10:54:33 +00:00
deanqx 936e53fe2f docs: update old "key new" to new "key create" command (#1487)
Fix invalid references to the `garage key new` commands. Mainly on documentation side except one script: `script/dev-bucket.sh`.

This is my first pull request ever. I'm open for feedback, let me know if I should change anything about this PR.

Co-authored-by: deanqx <110404616+deanqx@users.noreply.github.com>
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1487
2026-07-07 10:34:42 +00:00
Alex bbc1dc3d6f Merge pull request 'TypedTree' (#1456) from krtab/garage:typed_tree into main-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1456
Reviewed-by: Armael <armael@noreply.localhost>
Reviewed-by: Alex <lx@deuxfleurs.fr>
2026-06-30 21:04:06 +00:00
Arthur Carcano 7cdcaa8ebe Panic on (reoccuring) decode error 2026-06-30 21:04:06 +00:00
Arthur Carcano 4133476ec9 Add TODO comment for migration to typedtree 2026-06-30 21:04:06 +00:00
Arthur Carcano b277d49ad6 Add more type invariants to RcEntry and new RcState 2026-06-30 21:04:06 +00:00
Arthur Carcano 5a4da29f92 Migrate BlockRc and BlockResyncManager to TypedTree 2026-06-30 21:04:06 +00:00
Arthur Carcano eb91f463f5 Add TypedTree 2026-06-30 21:04:06 +00:00
Alex 22e003f0d0 Merge pull request 'k2v-client: misc fixes' (#1463) from k2v-client-clonable into main-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1463
2026-06-30 21:02:44 +00:00
Armaël Guéneau 0ebf60f709 k2v-client: re-export crates hyper_rustls and hyper_util 2026-06-30 21:02:44 +00:00
Armaël Guéneau d588a96125 k2v-client: implement Debug for K2vClient 2026-06-30 21:02:44 +00:00
Armaël Guéneau e665a3432c k2v-client: derive Clone for the K2vClient type 2026-06-30 21:02:44 +00:00
Armaël Guéneau 5884b4e9c7 k2v-client: repair build; serializers depend on the serde "std" features 2026-06-30 21:02:44 +00:00
Alex a32621c8fd Merge pull request 'fix(s3): report non-existent keys as deleted in bulk DeleteObjects (fix #1460)' (#1469) from smattymatty/garage:main-v2 into main-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1469
Reviewed-by: Alex <lx@deuxfleurs.fr>
2026-06-17 14:05:14 +00:00
Mathew Storm 8eb7628bf4 test(s3): cover bulk DeleteObjects of a non-existent key 2026-06-17 14:05:14 +00:00
Mathew Storm cd26594673 fix(s3): treat NoSuchKey as success in bulk DeleteObjects 2026-06-17 14:05:14 +00:00
Alex 1b28094f20 Merge pull request 'K2V: provide monotonic reads by default, with a flag to opt-out' (#1452) from Armael/garage:read_repair into main-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1452
Reviewed-by: Alex <lx@deuxfleurs.fr>
2026-06-17 14:00:13 +00:00
Armaël Guéneau 4f9faeb282 minor fixes 2026-06-17 14:00:13 +00:00
Armaël Guéneau 3c7990027a K2V: update docs to add read monotonicity guarantees & flags 2026-06-17 14:00:13 +00:00
Armaël Guéneau a159c1c483 add missing repair-on-read for k2v range reads 2026-06-17 14:00:13 +00:00
Armaël Guéneau 555e0826a2 define a separate enum for the monotonic/non-monotonic read flag 2026-06-17 14:00:13 +00:00
Armaël Guéneau d0f89068c6 repair_on_read: send multiple items to update in a single RPC 2026-06-17 14:00:13 +00:00
Armaël Guéneau 3e25914210 K2V: provide monotonic reads by default, with a flag to opt-out
This performs synchronous repair-on-read for K2V reads:
- uses the new get_*_monotonic operations in the table module
- implements repair-on-read for the K2V-specific poll operations
2026-06-17 14:00:13 +00:00
Armaël Guéneau 5500f1c412 table: disable asynchronous repair-on-read; add get_*_monotonic functions for synchronous repair-on-read 2026-06-17 14:00:13 +00:00
Armaël Guéneau 2d7c0a6087 table: also apply repair-on-read the first time a value is set 2026-06-17 14:00:13 +00:00
Wesley Hershberger 76aae3cb96 Drop parse_duration for fundu-systemd (#1246) (#1468)
`parse_duration` is no longer maintained upstream; `fundu-system` seems like the best option to provide most of the functionality provided by parse_duration and minimize deps.

Fixes #1246

Dep diffstat: +3 -6

## Caveats
- I've done basically no testing of this PR beyond `cargo test`
- See my comment in #1246 for regression risk; this is a breaking change. Is there a document I should update to make note of this? Should I rebase this for `next-v3` instead?
- In theory CVE-2021-29932 is fixed by this PR as `fundu-systemd` doesn't support exponents, but I've done no verification that this is really the case beyond the info in #1246

Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1468
Reviewed-by: Alex <lx@deuxfleurs.fr>
2026-06-17 13:57:21 +00:00
jo 1d1456f1d6 Add details to read file errors (#1475)
If the files configured in `api_token_file` or `rpc_secret_file` are not found, garage exits with no details (at least not enough) of what went wrong:

    2026-06-14T23:40:34.517429Z  INFO garage::server: Loading configuration from tmp/config1.toml...
    Error: IO error: No such file or directory (os error 2)

This add the kind of file and the file path being read, to the returned error message:

    2026-06-14T23:41:41.136213Z  INFO garage::server: Loading configuration from tmp/config1.toml...
    Error: Failed to read secret file /run/secrets/rpc_secret: No such file or directory (os error 2)

Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1475
Reviewed-by: Alex <lx@deuxfleurs.fr>
2026-06-17 13:53:43 +00:00
maximilien a379406522 Merge pull request 'fix reflected xss when returning errors on web endpoint' (#1471) from escape-errors into main-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1471
2026-06-11 06:50:52 +00:00
trinity-1686a da568cf56f fix reflected xss when returning errors on web endpoint
Reported by Filippo Decortes from ares-cyber.ai
2026-06-10 19:44:27 +02:00
Dave St.Germain 2bde733e09 fix: enable compilation on OpenBSD by removing keepalive interval (fix #1413) (#1453)
This fixes #1413 by conditionally compiling the section that sets a keepalive interval, which isn't supported on OpenBSD.

Tested on OpenBSD 7.8

Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1453
Reviewed-by: Alex <lx@deuxfleurs.fr>
2026-05-14 15:17:20 +00:00
Alex 91573eb028 Merge pull request 'replace Crdt impl on Option by explicit CancelingOption and MergingOption types' (#1451) from option-crdt into main-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1451
2026-05-13 09:56:29 +00:00
Alex Auvolat a646180d7e fix fuzz targets 2026-05-13 11:47:57 +02:00
Alex Auvolat bacc6c98b2 replace expiration field with custom type that merges to min value 2026-05-13 11:20:10 +02:00
Alex Auvolat bf0a24ea69 replace Option CRDT by explicit CancelingOption and MergingOption types 2026-05-13 11:20:06 +02:00
Arthur Carcano eb37a3e11a Fuzzing for K2VItem Crdt (#1438)
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1438
Reviewed-by: Alex <lx@deuxfleurs.fr>
2026-05-12 14:44:54 +00:00
smattymatty 54c63387cb fix(cors): include Access-Control-Allow-Headers in permissive OPTIONS placeholder (#1450)
The OPTIONS placeholder for buckets without a resolvable global alias returns` Access-Control-Allow-Origin: *` and `Access-Control-Allow-Methods: *` but omits `Access-Control-Allow-Headers`.

Bug verified against Garage v2.2.0 with a local-aliased bucket: OPTIONS placeholder doesn't have `Access-Control-Allow-Headers`, causes the browser to reject signed PUT preflights

The current placeholder fails open for unsigned simple requests but blocks every signed request, undermining the design intent flagged in the FIXME:

```rs
// We take the permissive approach of allowing everything,
// because we don't want to prevent web apps that use
// local bucket names from making API calls.
```

Adds `Access-Control-Allow-Headers: *` so the permissive default is actually permissive for the request shapes that exist in practice.

Refs #258. Does not address the broader FIXME (CORS rule resolution for local-aliased buckets); the placeholder approach is preserved.

All tests are fine locally:

```bash
 ▲ ~/opensource/garage cargo test -p garage_api_common cors::

running 5 tests
test cors::tests::preflight_with_single_allowed_origin_returns_request_origin ... ok
test cors::tests::preflight_with_multiple_allowed_origins_reflects_request_origin ... ok
test cors::tests::preflight_with_wildcard_allowed_origin_returns_wildcard ... ok
test xml::cors::tests::test_deserialize_norules ... ok
test xml::cors::tests::test_deserialize ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 16 filtered out; finished in 0.00s
```

Co-authored-by: smattymatty <smattymatt@gmail.com>
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1450
Reviewed-by: Alex <lx@deuxfleurs.fr>
2026-05-12 08:17:48 +00:00
Alex Auvolat 84bdc9f50f Update Redoc to latest version (#1448)
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1448
2026-05-12 08:05:18 +00:00
Arthur Carcano 3a5f060693 Add bucket_alias CRDT fuzz target (#1439)
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1439
Reviewed-by: Alex <lx@deuxfleurs.fr>
2026-05-07 14:02:26 +00:00
Arthur Carcano 21d29a4cf6 Add fuzing for Key CRDT (#1444)
This has duplicated changes with #1442 that will likely conflict and need rebase.

Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1444
Reviewed-by: Alex <lx@deuxfleurs.fr>
2026-05-07 13:42:35 +00:00
Arthur Carcano a0887afc4f Add fuzing for AdminApiToken CRDT (#1443)
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1443
Reviewed-by: Alex <lx@deuxfleurs.fr>
2026-05-07 11:43:25 +00:00
Arthur Carcano f757991635 Add block_ref CRDT fuzz target (#1440)
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1440
Reviewed-by: Alex <lx@deuxfleurs.fr>
2026-05-07 11:27:14 +00:00
Arthur Carcano 0da317e3d5 Fuzz Bucket CRDT (#1442)
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1442
Reviewed-by: Alex <lx@deuxfleurs.fr>
2026-05-06 18:55:47 +00:00
Alex 57ceed38f3 Merge pull request 'Improvements to the fuzzing code' (#1437) from krtab/garage:fuzz_crdts into main-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1437
Reviewed-by: Alex <lx@deuxfleurs.fr>
2026-05-05 10:03:36 +00:00
Arthur Carcano 0eb7d61829 cargo fmt 2026-05-05 10:41:21 +02:00
Arthur Carcano 382981642d Remove uneeded clones 2026-05-04 17:26:13 +02:00
Arthur Carcano 28a75d7234 Add LXs corrolary 2026-05-04 17:23:59 +02:00
Arthur Carcano e996f34887 Factor the crdt test code 2026-05-04 17:23:31 +02:00
Arthur Carcano defaac1b4f Use PartialEq instead of crdt_state 2026-05-04 17:07:35 +02:00
Alex 9f157677c2 Merge pull request 'First CRDT fuzz: MPU and version tables' (#1411) from krtab/garage:fuzz_crdts into main-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1411
2026-05-01 21:36:17 +00:00
Alex Auvolat ddc42c89fb add #fuzz devshell and make fuzzing work on nixos 2026-05-01 21:36:17 +00:00
Arthur Carcano a25ad494cc Add fuzzing README 2026-05-01 21:36:17 +00:00
Arthur Carcano 7d97b2b96e Ignore flaky test_items_and_indices 2026-05-01 21:36:17 +00:00
Arthur Carcano a5650ea303 Ignore typos in fuzz/ 2026-05-01 21:36:17 +00:00
Arthur Carcano 322da7242b Post review fixes 2026-05-01 21:36:17 +00:00
Arthur Carcano 6a097e7de3 Add MPU table 2026-05-01 21:36:17 +00:00
Arthur Carcano 6ddae5397c Add version table fuzz 2026-05-01 21:36:17 +00:00
Arthur Carcano 9a18259419 Add rust toolchain toml in fuzz dir 2026-05-01 21:36:17 +00:00
Arthur Carcano ade4d07bb5 Set up fuzz infrastructure 2026-05-01 21:36:17 +00:00
Alex 0a5282d918 Merge pull request 'Add garage health CLI subcommand' (#1373) from Arlen2/garage:1354_health-check_cmd into main-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1373
2026-05-01 19:48:14 +00:00
Alex Auvolat 12012916b7 simplify the garage health subcommand 2026-05-01 21:32:50 +02:00
Paul FLORENCE 9fa4e03748 Add health-check command to garage CLI
This command is used to check the local node health.

Related to https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/1354
2026-05-01 21:32:50 +02:00
Alex f7be222471 Merge pull request 'admin api: return full layout computation statistics as json (fix #1428)' (#1435) from fix-1428 into main-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1435
2026-05-01 17:53:39 +00:00
Alex Auvolat 5e9380820e admin api: update OpenApi schema 2026-05-01 17:53:39 +00:00
Alex Auvolat 62349a6559 admin api: return full layout computation statistics as json (fix #1428) 2026-05-01 17:53:39 +00:00
Alex Auvolat ada0c8ab70 admin api: add fields to GetNodeInfo result (fix #1429) (#1434)
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1434
2026-05-01 16:57:27 +00:00
Alex Auvolat 7bc7f33f43 bg vars: return "never" when scrub never ran (fix #1421) (#1430)
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1430
2026-05-01 15:06:02 +00:00
Alex Auvolat be203494c5 set some flaky tests as #[ignore] (#1432)
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1432
2026-05-01 14:44:28 +00:00
Alex Auvolat 3c983ac5e0 admin api: properly eliminate irrelevant role deletions (fix #1427) (#1431)
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1431
2026-05-01 14:40:34 +00:00
Minkyu Kim a2c797000f fix(cors): return single matching origin instead of multiple values in Access-Control-Allow-Origin (#1419)
## Title
fix(cors): return single matching origin instead of multiple values in `Access-Control-Allow-Origin`

## Summary
This PR fixes bucket CORS responses when a single CORS rule contains multiple `AllowedOrigins`.

Previously, Garage returned the configured origins as a comma-separated list in `Access-Control-Allow-Origin`, for example:

```http
Access-Control-Allow-Origin: https://app.example.test, https://admin.example.test
```

This is not the expected browser-facing behavior.
When a request origin matches a configured rule, the response should reflect **only the matching request origin**, unless the rule contains `*`.

## What changed
- `Access-Control-Allow-Origin` now behaves as follows:
  - returns `*` when the matched rule contains a wildcard origin
  - otherwise returns the request `Origin` as a **single value**
- added `Vary: Origin` when ACAO reflects the request origin
- added preflight-specific `Vary` handling in the preflight path for:
  - `Origin`
  - `Access-Control-Request-Method`
  - `Access-Control-Request-Headers`

## Scope
This change applies to shared bucket CORS handling paths, including:
- S3 API responses
- K2V API responses
- S3 POST object responses
- web bucket responses
- preflight (`OPTIONS`) bucket CORS responses

This does **not** change admin API fixed CORS behavior.

## Reproduction
A direct repro script is included:

```bash
./script/test-cors-multi-origin.sh
```

It exercises two cases against a direct single-node Garage instance:

1. **single-origin control**
2. **multi-origin repro**

Before this fix, the multi-origin case returned a comma-separated ACAO value.

After this fix, both cases reflect only the request origin.

## Example behavior

### Before
```http
Access-Control-Allow-Origin: https://app.example.test, https://admin.example.test
```

### After
```http
Access-Control-Allow-Origin: https://app.example.test
```

## Tests
Added/updated tests in `src/api/common/cors.rs` for:
- single-origin control
- multiple allowed origins reflecting the request origin
- wildcard origin preserving `*`
- preserving existing `Vary` values while appending `Origin`

## Validation
Used for validation:

```bash
cargo test -p garage_api_common cors::tests -- --nocapture
cargo build -p garage --bin garage
./script/test-cors-multi-origin.sh
```

## Reproducibility
For reviewers who want to validate behavior by commit:

- Before fix: `aa368e4b`
  - includes the direct repro script and the regression test setup
  - multi-origin ACAO is reproduced as a comma-separated value

- After fix: `f630eb92`
  - reflects only the matching request origin
  - preserves wildcard behavior
  - adds `Vary: Origin` and preflight-specific `Vary` handling

Branch:
- `fix/cors-multiple-allow-origin`

Base used during validation:
- `74ad3bf8` (`main-v2`)

Closes Deuxfleurs/garage#1149

Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1419
2026-04-28 14:48:02 +00:00
Austin Drummond 80f9335950 collapse sequential whitespace in canonical SigV4 header values (#1424)
## Summary

Garage's SigV4 canonical-request builder trims leading/trailing whitespace from signed header values but does not collapse sequential internal whitespace, which the SigV4 spec requires:

> Convert sequential spaces to a single space.

— https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html

AWS SDKs apply this normalization before computing the signature, but transmit the raw value on the wire. The receiver must therefore apply the same normalization when reconstructing the canonical request, otherwise the recomputed hash differs and the request is rejected as `Invalid signature`.

Same class of canonicalization-drift bug as #1155 / !1382, but on the canonical-headers axis rather than the canonical-URI axis.

## Reproduction

Surfaces in practice with `gitlab-runner`'s S3 cache uploader. I was in the midst of migrating my runner cache from AWS S3 to garage, but I noticed some shared runner caches were no longer uploading.

I was using `sha256sum | sha256sum` to compute my cache keys, which leaves a trailing `  -` on the value. Once GitLab appends `-protected` for protected branches the resulting `x-amz-meta-cachekey` header value contains internal sequential whitespace and triggers the mismatch:

```
x-amz-meta-cachekey:php-  --protected
                              ^^
                              two spaces, preserved by Garage
```

Without the fix the included regression test (`test_presigned_put_with_user_metadata`) fails with HTTP 403; with the fix it returns 200.

`aws-cli` is unaffected because it signs `Content-Type` rather than user metadata, so the specific code path with whitespace-bearing signed header values isn't exercised.

## Fix

In `canonical_request` (`src/api/common/signature/payload.rs`), replace the `.trim()` call on the joined header value with the full SigV4 normalization — `split_whitespace().collect::<Vec<_>>().join(" ")` — which both trims edges and collapses internal runs.

## Tests

* New regression test `test_presigned_put_with_user_metadata` covering a  presigned PUT whose `x-amz-meta-*` value contains internal sequential whitespace.
* Full integration suite passes: `40 passed; 0 failed; 2 ignored`.
* `garage_api_common` unit tests pass: `18 passed; 0 failed`.

## Notes

* Backwards-compatible: any signature that validated before still validates, because clients are spec-required to collapse on their side; Garage was only rejecting requests where the client had collapsed correctly but Garage hadn't.
* No config or migration changes.
* Fix applies to both presigned-URL and Authorization-header code paths since they share the canonical-request builder.

Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1424
Reviewed-by: Alex <lx@deuxfleurs.fr>
2026-04-27 21:15:23 +00:00
maximilien d217a3f15d add SECURITY.md (#1423)
Add some instructions to report security issues with garage.

Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1423
2026-04-27 07:33:27 +00:00
Alex Auvolat 063bf8258b write CONTRIBUTING.md file, first iteration (#1406)
Merging this first version as a baseline. For future work: write a security.md document to explain how to report security vulnerabilities, and split off the release process in a separate releasing.md document

Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1406
2026-04-26 10:35:17 +00:00
Alex 1d66240495 Merge pull request 'Update dependencies post-2.3.0 release' (#1415) from update-dependencies into main-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1415
2026-04-23 20:42:22 +00:00
Alex Auvolat d977ca4a24 fix new cargo clippy lints 2026-04-23 22:21:15 +02:00
Alex Auvolat 1cdaccbc3d update rust-overlay and use rust 1.95.0 2026-04-23 21:17:27 +02:00
Alex Auvolat 8e38680ef5 update dependencies post-2.3.0 release and update to rust 1.91.1 2026-04-23 21:11:19 +02:00
bnjoroge1 393c4bb2f6 cli: hide secret env values in help (#1418)
Closes #1417.

Co-authored-by: bnjoroge1 <bnjoroge1@users.noreply.git.deuxfleurs.fr>
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1418
2026-04-23 18:52:54 +00:00
Arthur Carcano 74ad3bf887 Replace the existential lifetime in sqlite adapter with a static one (#1407)
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1407
Reviewed-by: Alex <lx@deuxfleurs.fr>
2026-04-20 09:28:46 +00:00
Yureka 7c18abb664 fix: prevent depending on aws-lc via reqwest (#1412)
Otherwise the rustls dependency might be built with both aws-lc and ring backends,
leading to the following error in the k2v_client tests when
consul-discovery feature is enabled (including the reqwest dependency):

```
Could not automatically determine the process-level CryptoProvider from Rustls crate features.
Call CryptoProvider::install_default() before this point to select a provider manually, or make sure exactly one of the 'aws-lc-rs' and 'ring' features is enabled.
See the documentation of the CryptoProvider type for more information.
```

Co-authored-by: Yureka <yuka@yuka.dev>
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1412
Reviewed-by: Alex <lx@deuxfleurs.fr>
2026-04-20 09:28:21 +00:00
maximilien 1dffcca430 Merge pull request 'helm: make garage.toml bind addresses configurable via values' (#1383) from giottolino/garage:helm-configurable-bind-addrs into main-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1383
2026-04-19 22:39:59 +00:00
gi8 5a8ee9f640 helm: make garage.toml bind addresses configurable via values 2026-04-19 22:39:59 +00:00
161 changed files with 5781 additions and 1909 deletions
+12
View File
@@ -53,3 +53,15 @@ steps:
- nix-build -j4 --attr flakePackages.dev - nix-build -j4 --attr flakePackages.dev
- nix-shell --attr ci --run ./script/test-smoke.sh || (cat /tmp/garage.log; false) - nix-shell --attr ci --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
depends_on: [ build ] 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
+231
View File
@@ -0,0 +1,231 @@
# Contributing to Garage
## Policy on AI
To ensure the quality of the codebase and documentation, the use of AI,
including LLMs and coding agents, is strictly restricted in the following way:
- AI **must not** be used to write documentation
- **Do not** use AI to write bug reports, commit descriptions and pull request
messages
- **Do not** use AI agents to make contributions to Garage, all contributions
must be led by a human that know what they are doing at all times
- AI **may** be used for some tedious code generation tasks, limited to very
mechanical translations from API docs or boilerplate writing. The code
generated must be so simple as to make it clear that it cannot be covered by
copyright.
You are free to make use of AI privately to explore the codebase and solve
conceptual problems, but please restrain from copying the output from an LLM
anywhere in your code or on the issue tracker, or from letting an agent edit
the codebase directly.
## Asking questions
Read the documentation before asking questions.
Do not use the issue tracker to ask questions about Garage.
Questions asked on the issue tracker will be closed.
Ask questions on the Matrix channel `#garage:deuxfleurs.fr` so that any
community member can see your question and help you out.
If you need in-depth support from the Garage developers specifically, write to
`garagehq@deuxfleurs.fr`. Even if you do so, we do not commit to giving you an
answer.
## Reporting bugs
When writing a bug report, use this checklist:
- For bugs that can be reproduced:
- confirm that you are using the latest version of Garage and that the bug still exists in this version
- set the log level to debug using the `RUST_LOG=garage=debug` environment variable and reproduce the bug to get more verbose logs
- Check whether there is already an open issue in the bug tracker. If so, your bug report is still valuable but please add it as a comment to the existing issue instead of opening a new one.
- Collect as much information as possible:
- logs of the Garage daemon at the time the issue happened, including logs that show what was happening before the issue occurred
- the output of `garage status`
- the output of `garage stats -a`
- the output of `garage layout history`
- Write a detailed bug report, including:
- a description of your cluster (number of nodes, hardware, operating system, networking, etc)
- a detailed description of what you did that led to the issue, including any code or command line that invoked a Garage API
- what you were expecting
- what actually happened, and how that's different from what you expected
- the information collected previously
- if possible, simple steps to help the developers reproduce the issue locally
Bug reports that are imprecise or otherwise unactionable will be closed.
## Suggesting new features
Garage can be improved in many ways, but just suggesting a new feature does not mean we will implement it.
Feature requests that may lead to an actual implementation are feature requests that:
- are precise and actionable, i.e. include a precise description of the expected behavior and any necessary architectural details required for the implementation
- are motivated by actual need from a variety of users
Moreover, a certain number of features are defined as out-of-scope for Garage, including but not limited to:
- extensions to the S3 API that are not present on AWS
- features that require the implementation of a consensus algorithm
- more generally, features that are incompatible with the architecture of Garage and its goal of staying simple
Only feature requests in one of the following category may stay open in the issue tracker:
- features that the Garage team wants to work on
- features that are being actively worked on by an external contributor which is clearly identified
- features that are easy to implement and could be an easy task for a new contributor that wants to get to know the codebase
All other feature requests will be closed after a few months of inactivity, so as to keep the number of open issues to a manageable level.
Feature requests that are clearly out of scope will be closed directly.
## Improving the documentation
An easy way to contribute to Garage which also adds a lot of value is to
improve the documentation. Make sure to write in clear technical English, and
write unambiguously. Documentation contributions are very appreciated if they
are well-written.
## For developers
We welcome code contributions to Garage that adhere to our standards for quality:
- Changes should be reviewed from a functional perspective to ensure that they work well with the existing codebase and do not introduce bugs or subtle issues.
- You must have tested your contribution to make sure that it does what it says. The amount of testing required is proportional to the complexity of the change introduced.
- Any new feature must be properly documented following existing practices (see below).
- Unit tests should be included when relevant.
- Contributions should pass basic lints for syntactic quality (`cargo fmt`, `cargo clippy`, `typos`).
- Contributions should pass our CI test suite.
- No user-facing breaking changes may be introduced between major releases.
- No internal data model change may be introduced between major releases, to
ensure that Garage daemons with different minor/patch versions numbers can
work together in a cluster. For major releases, a proper migration path
should be implemented and tested thoroughly.
Please follow up on your work when changes are requested, to avoid stale PRs.
Do not take it personally if a Garage developer pushes directly to your branch
to modify your contribution, as this might be necessary to get it merged
faster.
### Properly documenting your contribution
#### Configuration options
New configuration options should be documented in
`doc/book/reference-manual/configuration.md`. The documentation for a
configuration option should be exhaustive. For instance, for choice options all
choices should be listed explicitly with a precise description of their
meaning.
In terms of syntax, all configuration options should appear in three places:
- in the example at the top, with an example value
- in the index of all configuration options which is sorted by alphabetical order
- in its dedicated subsection with full reference text
#### CLI commands and command flags
CLI commands are self-documented using the doc commends in the codebase.
Make sure to write clear and precise comments for all options you are adding.
#### S3 features
If you implement new S3 features, make sure to update the compatibility matrix in `doc/book/reference-manual/s3-compatibility.md`.
#### Admin API
The admin API has an OpenAPI specification that is automatically generated
using Utoipa, from a description of each endpoint that is given in
`src/api/admin/openapi.rs` and a description of data structure schemas in
`src/api/admin/api.rs`. The code in `openapi.rs` is only used to generate the
OpenAPI specification document and not for the actual implementation in Garage,
whereas structures defined in `api.rs` are also used for the implementation of
API calls. Make sure to write good doc comments for all of these items so that
the OpenAPI specification will be precise and accurate.
An up-to-date version of the OpenAPI specification document should be kept in
the repository in `doc/api/garage-admin-v2.json`. When you are making changes
to the admin API, update this document with the following command:
```
cargo run -- admin-api-schema > doc/api/garage-admin-v2.json
```
## Garage team organization
Alex (handle `lx`) is the lead developer and is responsible of ensuring the
correctness of Garage and stability between version upgrades.
The other maintainers are Trinity (handle `trinity-1686a`), Quentin (handle `quentin`) and Maximilien (handle `halfa`).
Maximilien is responsible for coordinating effort on the Kubernetes integration / Helm chart.
## Pull request merging criteria
The following PRs should only be merged after review and approval from Alex:
- PRs that introduce architectural changes, such as changes in the data model
or change in the coordination protocols between nodes
- PRs that introduce changes on the format of data structures used for
persistent disk storage and internal cluster communication (RPC)
- PRs that are suspected of introducing some kind of breakage or unexpected
behavior due to their complexity
PRs that introduce breaking change for users but don't fall in one of the
previous category should be discussed between maintainers to evaluate the
impact on users when upgrading. Alex's approval is not required to merge them
as long as they are clearly identified as breaking in the PR title, and are
properly merged in the branch for the next major version and not in the current
main branch.
All other PRs can be merged by any maintainer on their own, once they are
confident that the quality standards defined in this document are respected
before merging.
## Merging strategy
When merging PRs, maintainers should ensure that a Git commit is created by
Forgejo that records the PR number, its title and its text in the commit
message. If a PR is fixing an issue, make sure that the issue number is
included in the PR title as well. This is to ensure that when releasing a new
version of Garage, the changelog in the release notes can be properly
constructed by reading the Git log since the last release.
We also want to keep the history "almost linear" to facilitate the use of `git
bisect` if it ever were necessary. This leaves the following two merging
strategies:
- For PRs that consist of many commits that should stay independent, the
"rebase and create merge commit" strategy should be used. The merge commit is
created automatically by Forgejo and saves the PR's number, title and text in
the commit message.
- For PRs that consist of only one commit, or a few number of commits that can
be merged, the "create squash commit" strategy should be used. This way a
single commit will be created by Forgejo which also saves the PR's number,
title and text in the commit message.
When cherry-picking commits from one branch to the other, a simple fast-forward
merging strategy can be used if the commit message already references a PR
number.
Generated
+666 -696
View File
File diff suppressed because it is too large Load Diff
+7 -2
View File
@@ -16,6 +16,7 @@ members = [
"src/garage", "src/garage",
"src/k2v-client", "src/k2v-client",
"src/format-table", "src/format-table",
"fuzz",
] ]
default-members = ["src/garage"] default-members = ["src/garage"]
@@ -40,6 +41,7 @@ k2v-client = { version = "0.0.4", path = "src/k2v-client" }
# External crates from crates.io # External crates from crates.io
arc-swap = "1.8" arc-swap = "1.8"
arbitrary = { version = "1.4.2"}
argon2 = "0.5" argon2 = "0.5"
async-trait = "0.1" async-trait = "0.1"
backtrace = "0.3" backtrace = "0.3"
@@ -51,20 +53,23 @@ cfg-if = "1.0"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
crc-fast = "1.9" crc-fast = "1.9"
crypto-common = "0.1" crypto-common = "0.1"
fundu = "2.0"
fundu-systemd = "0.3"
gethostname = "1.1" gethostname = "1.1"
git-version = "0.3" git-version = "0.3"
hex = "0.4" hex = "0.4"
hexdump = "0.1" hexdump = "0.1"
html-escape = "0.2.13"
hmac = "0.12" hmac = "0.12"
itertools = "0.14" itertools = "0.14"
ipnet = "2.11" ipnet = "2.11"
lazy_static = "1.5" lazy_static = "1.5"
libfuzzer-sys = "0.4"
md-5 = "0.10" md-5 = "0.10"
mktemp = "0.5" mktemp = "0.5"
nix = { version = "0.31", default-features = false, features = ["fs"] } nix = { version = "0.31", default-features = false, features = ["fs"] }
nom = "8.0" nom = "8.0"
parking_lot = "0.12" parking_lot = "0.12"
parse_duration = "2.1"
paste = "1.0" paste = "1.0"
pin-project = "1.1" pin-project = "1.1"
pnet_datalink = "0.35" pnet_datalink = "0.35"
@@ -113,7 +118,7 @@ kube = { version = "3.0", default-features = false, features = [
] } ] }
schemars = "1.2" schemars = "1.2"
reqwest = { version = "0.13", default-features = false, features = [ reqwest = { version = "0.13", default-features = false, features = [
"rustls", "rustls-no-provider",
"json", "json",
] } ] }
+45
View File
@@ -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)
+14
View File
@@ -0,0 +1,14 @@
# Security Reporting
If you wish to report responsibly a security vulnerability about Garage, we ask that you follow the following process.
Please report each security vulnerabilities by filling out the following template:
- PROJECT: A URL to the code repository containing the vulnerable version - be reminded that the source of truth is at https://git.deuxfleurs.fr/deuxfleurs/garage
- PUBLIC: Please let us know if this vulnerability has been made or discussed publicly already, and if so, please let us know where.
- DESCRIPTION: Please provide precise description of the security vulnerability you have found with as much information as you are able and willing to provide.
Please send the above info, along with any other information you feel is pertinent by emailing the core team at: garagehq@deuxfleurs.fr
The Garage Core Team will let you know within a few weeks whether or not your report has been accepted or rejected.
We ask that you please keep the report confidential until we have either responded or made a public announcement.
+196
View File
@@ -1797,6 +1797,17 @@
"type": "string" "type": "string"
}, },
"description": "Plain-text information about the layout computation\n(do not try to parse this)" "description": "Plain-text information about the layout computation\n(do not try to parse this)"
},
"statistics": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/ComputationStat",
"description": "Structured statistics about the layout computation"
}
]
} }
} }
}, },
@@ -2119,6 +2130,180 @@
"Historical" "Historical"
] ]
}, },
"ComputationStat": {
"type": "object",
"required": [
"replicationFactor",
"effectiveZoneRedundancy",
"partitionSize",
"lowPartitionSize",
"usableCapacity",
"totalCapacity",
"effectiveCapacity",
"lowUsableCapacity",
"zones"
],
"properties": {
"effectiveCapacity": {
"type": "integer",
"format": "int64",
"description": "The final effective capacity of the cluster, accounting for replication",
"minimum": 0
},
"effectiveZoneRedundancy": {
"type": "integer",
"description": "The zone redundancy factor achieved by this layout",
"minimum": 0
},
"lowPartitionSize": {
"type": "boolean",
"description": "Warning flag indicating when partitions are very small"
},
"lowUsableCapacity": {
"type": "boolean",
"description": "Warning flag indicating that the raw node capacity could not be used\neffectively"
},
"partitionSize": {
"type": "integer",
"format": "int64",
"description": "The size of a partition, in bytes",
"minimum": 0
},
"previousPartitionSize": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "The size of a partition, in bytes, in the previous layout",
"minimum": 0
},
"replicationFactor": {
"type": "integer",
"description": "The cluster's replication factor",
"minimum": 0
},
"totalCapacity": {
"type": "integer",
"format": "int64",
"description": "The total raw capacity of nodes",
"minimum": 0
},
"totalMovedPartitions": {
"type": [
"integer",
"null"
],
"description": "The total number of partitions that will be moved to a new storage node",
"minimum": 0
},
"usableCapacity": {
"type": "integer",
"format": "int64",
"description": "The portion of total raw node capacity that is used by partitions",
"minimum": 0
},
"zones": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ComputationStatZone"
},
"description": "Per-zone storage statistics"
}
}
},
"ComputationStatNode": {
"type": "object",
"required": [
"id",
"tags",
"storedPartitions",
"newPartitions",
"totalCapacity",
"usableCapacity"
],
"properties": {
"id": {
"type": "string",
"description": "The node's ID"
},
"newPartitions": {
"type": "integer",
"description": "The number of partitions that are newly replicated on this node",
"minimum": 0
},
"storedPartitions": {
"type": "integer",
"description": "The number of partitions that are replicated on this node",
"minimum": 0
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "The node's tags as defined in the layout"
},
"totalCapacity": {
"type": "integer",
"format": "int64",
"description": "The node's raw capacity",
"minimum": 0
},
"usableCapacity": {
"type": "integer",
"format": "int64",
"description": "The portion of the node's raw capacity that is used by partitions it stores",
"minimum": 0
}
}
},
"ComputationStatZone": {
"type": "object",
"required": [
"name",
"nodes",
"totalReplicatedPartitions",
"uniquePartitions",
"totalCapacity",
"usableCapacity"
],
"properties": {
"name": {
"type": "string",
"description": "The name of the zone"
},
"nodes": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ComputationStatNode"
},
"description": "Per-node storage statistics for nodes in this zone"
},
"totalCapacity": {
"type": "integer",
"format": "int64",
"description": "The total raw capacity of nodes in this zone",
"minimum": 0
},
"totalReplicatedPartitions": {
"type": "integer",
"description": "The total number of partition replicas in this zone",
"minimum": 0
},
"uniquePartitions": {
"type": "integer",
"description": "The number of unique partitions that have at least one replica in this zone",
"minimum": 0
},
"usableCapacity": {
"type": "integer",
"format": "int64",
"description": "The used portion of the raw capacity of nodes in this zones",
"minimum": 0
}
}
},
"ConnectClusterNodesRequest": { "ConnectClusterNodesRequest": {
"type": "array", "type": "array",
"items": { "items": {
@@ -4221,6 +4406,17 @@
"newLayout": { "newLayout": {
"$ref": "#/components/schemas/GetClusterLayoutResponse", "$ref": "#/components/schemas/GetClusterLayoutResponse",
"description": "Details about the new cluster layout" "description": "Details about the new cluster layout"
},
"statistics": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/ComputationStat",
"description": "Structured statistics about the layout computation"
}
]
} }
} }
} }
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -547,7 +547,7 @@ ejabberdctl module_install mod_s3_upload
Create the required key and bucket with: Create the required key and bucket with:
```bash ```bash
garage key new --name ejabberd garage key create ejabberd
garage bucket create objects.xmpp-server.fr garage bucket create objects.xmpp-server.fr
garage bucket allow objects.xmpp-server.fr --read --write --key ejabberd garage bucket allow objects.xmpp-server.fr --read --write --key ejabberd
garage bucket website --allow objects.xmpp-server.fr 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: This is the usual Garage setup:
```bash ```bash
garage key new --name pleroma-key garage key create pleroma-key
garage bucket create pleroma garage bucket create pleroma
garage bucket allow pleroma --read --write --owner --key pleroma-key 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: This is the usual Garage setup:
```bash ```bash
garage key new --name pictrs-key garage key create pictrs-key
garage bucket create pictrs-data garage bucket create pictrs-data
garage bucket allow pictrs-data --read --write --key pictrs-key garage bucket allow pictrs-data --read --write --key pictrs-key
``` ```
+1 -1
View File
@@ -22,7 +22,7 @@ Note that `git-annex` requires to be compiled with Haskell package version
`aws-0.24` to work with Garage. `aws-0.24` to work with Garage.
```bash ```bash
garage key new --name my-key garage key create my-key
garage bucket create my-git-annex garage bucket create my-git-annex
garage bucket allow my-git-annex --read --write --key my-key garage bucket allow my-git-annex --read --write --key my-key
``` ```
+1 -1
View File
@@ -268,7 +268,7 @@ duck --delete garage:/my-files/an-object.txt
## WinSCP (libs3) {#winscp} ## 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: How to use `winscp.com`, the CLI interface of WinSCP:
+1 -1
View File
@@ -27,7 +27,7 @@ which support storing metrics in an object store:
This can be configured with Garage with the following: This can be configured with Garage with the following:
```bash ```bash
garage key new --name vector-system-logs garage key create vector-system-logs
garage bucket create system-logs garage bucket create system-logs
garage bucket allow system-logs --read --write --key vector-system-logs garage bucket allow system-logs --read --write --key vector-system-logs
``` ```
+7
View File
@@ -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). 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 ## How exposed websites work
Our website serving logic is as follow: Our website serving logic is as follow:
+1 -1
View File
@@ -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 as unrecoverable, and to delete them properly from the data store. This can be done
using the `garage block purge` command. 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 In [multi-HDD setups](@/documentation/operations/multi-hdd.md), to ensure that
data blocks are well balanced between storage locations, you may run a data blocks are well balanced between storage locations, you may run a
+5 -4
View File
@@ -68,10 +68,11 @@ To rebalance data, two strategies can be used:
secondary directory. This might never end up rebalancing everything if there secondary directory. This might never end up rebalancing everything if there
are data blocks that are only read and never written. are data blocks that are only read and never written.
- Active rebalancing: an operator of a Garage node can explicitly launch a repair - Active rebalancing: an operator of a Garage node can [explicitly launch a
procedure that rebalances the data directories, moving all blocks to their repair procedure](@/documentation/operations/durability-repairs.md#rebalance)
primary location. Once done, all secondary locations for all hash slices are that rebalances the data directories, moving all blocks to their primary
removed so that they won't be checked anymore when looking for a data block. 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 ## Read-only storage locations
+11 -6
View File
@@ -133,12 +133,17 @@ Use the following command to launch the Garage server:
garage server --single-node --default-bucket garage server --single-node --default-bucket
``` ```
The `--single-node` flag instructs Garage to automatically configure a single-node cluster without data replication. - the `--single-node` flag instructs Garage to automatically configure a
The `--default-bucket` flag instructs Garage to create a default access key and a default bucket using the environment variables we defined above. single-node cluster without data replication;
Both flags are optional and can be omitted, in which case you will have to follow manual configuration steps described below. - 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 can refer to the [manual configuration
you must follow the manual configuration steps. > 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, 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: 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 This section provides instructions that are equivalent to using the
`--single-node` and `--default-bucket` flags for automatic configuration. If `--single-node` and `--default-bucket` flags for automatic configuration. If
+4 -1
View File
@@ -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`) Garage daemon send its logs to `journald` (using the native protocol of `systemd-journald`)
instead of printing to stderr. 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 The following environment variables can be used to override the corresponding
values in the configuration file: 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, 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. or in [`metadata_snapshots_dir`](#metadata_snapshots_dir) if it is set.
This parameter can take any duration string that can be parsed by 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 Snapshots can allow to recover from situations where the metadata DB file is
corrupted, for instance after an unclean shutdown. See [this corrupted, for instance after an unclean shutdown. See [this
+5 -6
View File
@@ -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 many small values in buckets (in opposition to S3 which is more designed
to store large blobs). to store large blobs).
K2V is currently disabled at compile time in all builds, as the K2V is included in release builds since version 0.8.0. Precompiled builds
specification is still subject to changes. To build a Garage version with of earlier versions including `k2v` can be found in our download page under
K2V, the Cargo feature flag `k2v` must be activated. Special builds with "Extra builds": they can be easily identified as their tag name ends with
the `k2v` feature flag enabled can be obtained from our download page under `-k2v` (example: `v0.7.2-k2v`). Otherwise, when compiling Garage, the Cargo
"Extra builds": such builds can be identified easily as their tag name ends feature flag `k2v` must be activated.
with `-k2v` (example: `v0.7.2-k2v`).
The specification of the K2V API can be found The specification of the K2V API can be found
[here](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/commit/f8be15c37db857e177d543de7be863692628d567/doc/drafts/k2v-spec.md). [here](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/commit/f8be15c37db857e177d543de7be863692628d567/doc/drafts/k2v-spec.md).
+47
View File
@@ -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 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. 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 ## Important details
**THIS SECTION CONTAINS A FEW WARNINGS ON THE K2V API WHICH ARE IMPORTANT **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 | | `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 Returns the item with specified partition key and sort key. Values can be
returned in either of two ways: 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). 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>`** **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. 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 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 items to get (to get single items, set `singleItem` to `true`). A search is a
JSON struct with the following fields: JSON struct with the following fields:
@@ -711,6 +750,14 @@ HTTP/1.1 200 OK
Polls a range of items for changes. 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: The query body is a JSON object consisting of the following fields:
| name | default value | meaning | | name | default value | meaning |
Generated
+4 -4
View File
@@ -81,17 +81,17 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1763952169, "lastModified": 1776914043,
"narHash": "sha256-+PeDBD8P+NKauH+w7eO/QWCIp8Cx4mCfWnh9sJmy9CM=", "narHash": "sha256-qug5r56yW1qOsjSI99l3Jm15JNT9CvS2otkXNRNtrPI=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "ab726555a9a72e6dc80649809147823a813fa95b", "rev": "2d35c4358d7de3a0e606a6e8b27925d981c01cc3",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "ab726555a9a72e6dc80649809147823a813fa95b", "rev": "2d35c4358d7de3a0e606a6e8b27925d981c01cc3",
"type": "github" "type": "github"
} }
}, },
+10 -2
View File
@@ -6,9 +6,9 @@
inputs.nixpkgs.url = inputs.nixpkgs.url =
"github:NixOS/nixpkgs/cfe2c7d5b5d3032862254e68c37a6576b633d632"; "github:NixOS/nixpkgs/cfe2c7d5b5d3032862254e68c37a6576b633d632";
# Rust overlay as of 2025-11-24 # Rust overlay as of 2026-04-23
inputs.rust-overlay.url = inputs.rust-overlay.url =
"github:oxalica/rust-overlay/ab726555a9a72e6dc80649809147823a813fa95b"; "github:oxalica/rust-overlay/2d35c4358d7de3a0e606a6e8b27925d981c01cc3";
inputs.rust-overlay.inputs.nixpkgs.follows = "nixpkgs"; inputs.rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
# Crane as of 2025-01-24 # Crane as of 2025-01-24
@@ -95,6 +95,14 @@
killall killall
]; ];
}; };
# dev shell for fuzzing
fuzz = pkgs.mkShell {
buildInputs = with pkgs; [
targets.toolchainNightly
cargo-fuzz
];
};
}; };
}); });
} }
+4
View File
@@ -0,0 +1,4 @@
target
corpus
artifacts
coverage
+73
View File
@@ -0,0 +1,73 @@
[package]
name = "garage-fuzz"
version = "0.0.0"
publish = false
edition = "2018"
[package.metadata]
cargo-fuzz = true
[dependencies]
arbitrary = { workspace = true, features = ["derive"]}
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", "k2v"] }
[[bin]]
name = "version_crdt"
path = "fuzz_targets/version_crdt.rs"
test = false
doc = false
bench = false
[[bin]]
name = "mpu_crdt"
path = "fuzz_targets/mpu_crdt.rs"
test = false
doc = false
bench = false
[[bin]]
name = "bucket_crdt"
path = "fuzz_targets/bucket_crdt.rs"
test = false
doc = false
bench = false
[[bin]]
name = "block_ref_crdt"
path = "fuzz_targets/block_ref_crdt.rs"
test = false
doc = false
bench = false
[[bin]]
name = "admin_api_token_crdt"
path = "fuzz_targets/admin_api_token_crdt.rs"
test = false
doc = false
bench = false
[[bin]]
name = "key_crdt"
path = "fuzz_targets/key_crdt.rs"
test = false
doc = false
bench = false
[[bin]]
name = "bucket_alias_crdt"
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
+11
View File
@@ -0,0 +1,11 @@
# Fuzzing
## Setup
Install cargo fuzz: `cargo install cargo-fuzz`
## Launch
Run `cargo fuzz run <fuzz_target>` where `<fuzz_target>` is the name (without extension) of one of the `.rs` files in the `fuzz_targets` directory.
If you launch the command outside of the fuzz directory, you need to force the nightly toolchain with `cargo +nightly`.
+38
View File
@@ -0,0 +1,38 @@
#![no_main]
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<crdt::MergingOption<ExpirationTime>>,
crdt::Lww<AdminApiTokenScope>,
);
fn make(input: Input) -> AdminApiToken {
let (deleted, name, expiration, scope) = input;
let state = if deleted {
crdt::Deletable::Deleted
} else {
crdt::Deletable::present(AdminApiTokenParams {
created: 0,
token_hash: String::new(),
name,
expiration,
scope,
})
};
AdminApiToken {
prefix: String::new(),
state,
}
}
fuzz_target!(|inputs: (Input, Input, Input)| {
let (a, b, c) = inputs;
check_crdt_laws(make(a), make(b), make(c));
});
+20
View File
@@ -0,0 +1,20 @@
#![no_main]
use garage_fuzz::check_crdt_laws;
use garage_model::s3::block_ref_table::BlockRef;
use libfuzzer_sys::fuzz_target;
/// Build a BlockRef with a fixed block hash and version UUID so that CRDT state
/// can be compared across merge results. Only the deleted flag varies.
fn make_block_ref(deleted: bool) -> BlockRef {
BlockRef {
block: [0u8; 32].into(),
version: [0u8; 32].into(),
deleted: deleted.into(),
}
}
fuzz_target!(|inputs: (bool, bool, bool)| {
let (d1, d2, d3) = inputs;
check_crdt_laws(make_block_ref(d1), make_block_ref(d2), make_block_ref(d3));
});
+25
View File
@@ -0,0 +1,25 @@
#![no_main]
use garage_fuzz::check_crdt_laws;
use garage_model::bucket_alias_table::BucketAlias;
use garage_util::data::Uuid;
use libfuzzer_sys::fuzz_target;
/// Build a BucketAlias with a fixed name so that CRDT state can be compared
/// across merge results. The timestamp and optional bucket ID are the CRDT state.
fn make_bucket_alias(ts: u64, bucket_id: Option<[u8; 32]>) -> BucketAlias {
BucketAlias::new(String::new(), ts, bucket_id.map(Uuid::from))
}
fuzz_target!(|inputs: (
(u64, Option<[u8; 32]>),
(u64, Option<[u8; 32]>),
(u64, Option<[u8; 32]>)
)| {
let ((ts1, b1), (ts2, b2), (ts3, b3)) = inputs;
check_crdt_laws(
make_bucket_alias(ts1, b1),
make_bucket_alias(ts2, b2),
make_bucket_alias(ts3, b3),
);
});
+22
View File
@@ -0,0 +1,22 @@
#![no_main]
use garage_fuzz::check_crdt_laws;
use garage_model::bucket_table::{Bucket, BucketParams};
use garage_util::crdt::{self, Deletable};
use libfuzzer_sys::fuzz_target;
fn make(state: Deletable<BucketParams>) -> Bucket {
Bucket {
id: [0u8; 32].into(),
state,
}
}
fuzz_target!(|inputs: (
crdt::Deletable<BucketParams>,
crdt::Deletable<BucketParams>,
crdt::Deletable<BucketParams>
)| {
let (a, b, c) = inputs;
check_crdt_laws(make(a), make(b), make(c));
});
+36
View File
@@ -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));
});
+43
View File
@@ -0,0 +1,43 @@
#![no_main]
use garage_fuzz::check_crdt_laws;
use garage_model::key_table::{Key, KeyParams};
use garage_model::permission::{BucketKeyPerm, ExpirationTime};
use garage_util::crdt;
use garage_util::data::Uuid;
use libfuzzer_sys::fuzz_target;
type Input = (
bool,
crdt::Lww<String>,
crdt::Lww<crdt::MergingOption<ExpirationTime>>,
crdt::Lww<bool>,
crdt::Map<Uuid, BucketKeyPerm>,
crdt::LwwMap<String, crdt::CancelingOption<Uuid>>,
);
fn make(input: Input) -> Key {
let (deleted, name, expiration, allow_create_bucket, authorized_buckets, local_aliases) = input;
let state = if deleted {
crdt::Deletable::Deleted
} else {
crdt::Deletable::present(KeyParams {
created: None,
secret_key: String::new(),
name,
expiration,
allow_create_bucket,
authorized_buckets,
local_aliases,
})
};
Key {
key_id: String::new(),
state,
}
}
fuzz_target!(|inputs: (Input, Input, Input)| {
let (a, b, c) = inputs;
check_crdt_laws(make(a), make(b), make(c));
});
+37
View File
@@ -0,0 +1,37 @@
#![no_main]
use garage_fuzz::check_crdt_laws;
use garage_model::s3::mpu_table::{MpuPart, MpuPartKey, MultipartUpload};
use libfuzzer_sys::fuzz_target;
/// Build a MultipartUpload from an arbitrary deleted flag and parts list, using a fixed
/// upload_id/bucket_id/key so that CRDT state can be compared across merge results.
/// `MpuPart.version` is fixed to a constant since it is identity data, not CRDT state:
/// two replicas of the same part (same MpuPartKey) always share the same version UUID.
/// If deleted, parts are cleared to ensure a valid initial CRDT state.
fn make_mpu(deleted: bool, parts: Vec<(MpuPartKey, MpuPart)>) -> MultipartUpload {
let mut mpu = MultipartUpload::new(
[0u8; 32].into(),
0,
[0u8; 32].into(),
String::new(),
deleted,
);
for (key, mut part) in parts {
part.version = [0u8; 32].into();
mpu.parts.put(key, part);
}
if mpu.deleted.get() {
mpu.parts.clear();
}
mpu
}
fuzz_target!(|inputs: (
(bool, Vec<(MpuPartKey, MpuPart)>),
(bool, Vec<(MpuPartKey, MpuPart)>),
(bool, Vec<(MpuPartKey, MpuPart)>)
)| {
let ((d1, p1), (d2, p2), (d3, p3)) = inputs;
check_crdt_laws(make_mpu(d1, p1), make_mpu(d2, p2), make_mpu(d3, p3));
});
+42
View File
@@ -0,0 +1,42 @@
#![no_main]
use garage_fuzz::check_crdt_laws;
use garage_model::s3::version_table::{Version, VersionBacklink, VersionBlock, VersionBlockKey};
use libfuzzer_sys::fuzz_target;
/// Build a Version from an arbitrary deleted flag and block list, using a fixed uuid/backlink
/// so that CRDT state can be compared across merge results.
/// Duplicate block keys are dropped before construction.
/// If deleted, blocks are cleared to ensure a valid initial CRDT state.
fn make_version(deleted: bool, mut blocks: Vec<(VersionBlockKey, VersionBlock)>) -> Version {
blocks.sort_by_key(|(k, _)| *k);
blocks.dedup_by_key(|(k, _)| *k);
let mut v = Version::new(
[0u8; 32].into(),
VersionBacklink::Object {
bucket_id: [0u8; 32].into(),
key: String::new(),
},
deleted,
);
for (key, block) in blocks {
v.blocks.put(key, block);
}
if v.deleted.get() {
v.blocks.clear();
}
v
}
fuzz_target!(|inputs: (
(bool, Vec<(VersionBlockKey, VersionBlock)>),
(bool, Vec<(VersionBlockKey, VersionBlock)>),
(bool, Vec<(VersionBlockKey, VersionBlock)>)
)| {
let ((d1, b1), (d2, b2), (d3, b3)) = inputs;
check_crdt_laws(
make_version(d1, b1),
make_version(d2, b2),
make_version(d3, b3),
);
});
+2
View File
@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"
+56
View File
@@ -0,0 +1,56 @@
use garage_table::crdt::Crdt;
use std::fmt::Debug;
pub fn check_crdt_laws<T>(a: T, b: T, c: T)
where
T: Crdt + PartialEq + Clone + Debug,
{
// Idempotency: merge(a, a) == a
{
let mut a2 = a.clone();
a2.merge(&a);
assert_eq!(a2, a, "merge is not idempotent: {a2:#?} != {a:#?}");
}
// Commutativity: merge(a, b) == merge(b, a)
let ab = {
let mut t = a.clone();
t.merge(&b);
t
};
let ba = {
let mut t = b.clone();
t.merge(&a);
t
};
assert_eq!(ab, ba, "merge is not commutative: {ab:#?} != {ba:#?}");
// LX's corrolary: merge(merge(a,b),b) = merge(a,b)
let ab_b = {
let mut t = ab.clone();
t.merge(&b);
t
};
assert_eq!(ab, ab_b);
// Associativity: merge(merge(a, b), c) == merge(a, merge(b, c))
let ab_c = {
let mut t = ab;
t.merge(&c);
t
};
let bc = {
let mut t = b;
t.merge(&c);
t
};
let a_bc = {
let mut t = a;
t.merge(&bc);
t
};
assert_eq!(
ab_c, a_bc,
"merge is not associative: {ab_c:#?} != {a_bc:#?}"
);
}
+9 -1
View File
@@ -48,7 +48,7 @@ let
inherit (pkgs) lib stdenv; inherit (pkgs) lib stdenv;
toolchainFn = (p: p.rust-bin.stable."1.91.0".default.override { toolchainFn = (p: p.rust-bin.stable."1.95.0".default.override {
targets = lib.optionals (target != null) [ rustTarget ]; targets = lib.optionals (target != null) [ rustTarget ];
extensions = [ extensions = [
"rust-src" "rust-src"
@@ -148,6 +148,14 @@ let
in rec { in rec {
toolchain = toolchainFn pkgs; toolchain = toolchainFn pkgs;
toolchainNightly = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default.override {
targets = lib.optionals (target != null) [ rustTarget ];
extensions = [
"rust-src"
"rustfmt"
];
});
devShell = pkgs.mkShell { devShell = pkgs.mkShell {
buildInputs = [ buildInputs = [
toolchain toolchain
+2
View File
@@ -21,3 +21,5 @@
.idea/ .idea/
*.tmproj *.tmproj
.vscode/ .vscode/
# helm-unittest test suites
tests/
+4 -2
View File
@@ -2,7 +2,7 @@ apiVersion: v2
name: garage name: garage
description: S3-compatible object store for small self-hosted geo-distributed deployments description: S3-compatible object store for small self-hosted geo-distributed deployments
type: application type: application
version: 0.9.3 version: 0.9.4
appVersion: "v2.3.0" appVersion: "v2.3.0"
home: https://garagehq.deuxfleurs.fr/ home: https://garagehq.deuxfleurs.fr/
icon: https://garagehq.deuxfleurs.fr/images/garage-logo.svg icon: https://garagehq.deuxfleurs.fr/images/garage-logo.svg
@@ -15,4 +15,6 @@ keywords:
sources: sources:
- https://git.deuxfleurs.fr/Deuxfleurs/garage.git - https://git.deuxfleurs.fr/Deuxfleurs/garage.git
maintainers: [] maintainers:
- name: Garage maintainer team
email: garagehq@deuxfleurs.fr
+3
View File
@@ -33,11 +33,14 @@ S3-compatible object store for small self-hosted geo-distributed deployments
| garage.replicationFactor | string | `"3"` | Default to 3 replicas, see the replication_factor section at https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#replication_factor | | garage.replicationFactor | string | `"3"` | Default to 3 replicas, see the replication_factor section at https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#replication_factor |
| garage.consistencyMode | string | `"consistent"` | Default to read-after-write consistency, see the consistency_mode section at https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#consistency_mode | | garage.consistencyMode | string | `"consistent"` | Default to read-after-write consistency, see the consistency_mode section at https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#consistency_mode |
| garage.metadataAutoSnapshotInterval | string | `""` | 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. https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#metadata_auto_snapshot_interval | | garage.metadataAutoSnapshotInterval | string | `""` | 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. https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#metadata_auto_snapshot_interval |
| garage.admin.apiBindAddr | string | `"[::]:3903"` | |
| garage.rpcBindAddr | string | `"[::]:3901"` | | | garage.rpcBindAddr | string | `"[::]:3901"` | |
| garage.rpcSecret | string | `""` | If not given, a random secret will be generated and stored in a Secret object | | garage.rpcSecret | string | `""` | If not given, a random secret will be generated and stored in a Secret object |
| garage.s3.api.bindAddr | string | `"[::]:3900"` | |
| garage.s3.api.region | string | `"garage"` | | | garage.s3.api.region | string | `"garage"` | |
| garage.s3.api.rootDomain | string | `".s3.garage.tld"` | | | garage.s3.api.rootDomain | string | `".s3.garage.tld"` | |
| garage.s3.web.index | string | `"index.html"` | | | garage.s3.web.index | string | `"index.html"` | |
| garage.s3.web.bindAddr | string | `"[::]:3902"` | |
| garage.s3.web.rootDomain | string | `".web.garage.tld"` | | | garage.s3.web.rootDomain | string | `".web.garage.tld"` | |
| image.pullPolicy | string | `"IfNotPresent"` | | | image.pullPolicy | string | `"IfNotPresent"` | |
| image.repository | string | `"dxflrs/amd64_garage"` | default to amd64 docker image | | image.repository | string | `"dxflrs/amd64_garage"` | default to amd64 docker image |
+331
View File
@@ -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"
@@ -71,6 +71,13 @@ Create the name of the service account to use
{{- end }} {{- end }}
{{- end }} {{- end }}
{{/*
Extract the trailing port number from a bind address like [::]:3900 or 0.0.0.0:3900.
*/}}
{{- define "garage.portFromBindAddr" -}}
{{- regexFind "[0-9]+$" . -}}
{{- end }}
{{/* {{/*
Returns given number of random Hex characters. Returns given number of random Hex characters.
In practice, it generates up to 100 randAlphaNum strings In practice, it generates up to 100 randAlphaNum strings
+3 -3
View File
@@ -45,16 +45,16 @@ data:
[s3_api] [s3_api]
s3_region = "{{ .Values.garage.s3.api.region }}" s3_region = "{{ .Values.garage.s3.api.region }}"
api_bind_addr = "[::]:3900" api_bind_addr = "{{ .Values.garage.s3.api.bindAddr }}"
root_domain = "{{ .Values.garage.s3.api.rootDomain }}" root_domain = "{{ .Values.garage.s3.api.rootDomain }}"
[s3_web] [s3_web]
bind_addr = "[::]:3902" bind_addr = "{{ .Values.garage.s3.web.bindAddr }}"
root_domain = "{{ .Values.garage.s3.web.rootDomain }}" root_domain = "{{ .Values.garage.s3.web.rootDomain }}"
index = "{{ .Values.garage.s3.web.index }}" index = "{{ .Values.garage.s3.web.index }}"
[admin] [admin]
api_bind_addr = "[::]:3903" api_bind_addr = "{{ .Values.garage.admin.apiBindAddr }}"
{{- if .Values.monitoring.tracing.sink }} {{- if .Values.monitoring.tracing.sink }}
trace_sink = "{{ .Values.monitoring.tracing.sink }}" trace_sink = "{{ .Values.monitoring.tracing.sink }}"
{{- end }} {{- end }}
@@ -62,7 +62,9 @@ spec:
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- if and .Values.ingress.s3.api.enabled .Values.ingress.s3.web.enabled }}
--- ---
{{ end }}
{{- if .Values.ingress.s3.web.enabled -}} {{- if .Values.ingress.s3.web.enabled -}}
{{- $fullName := include "garage.fullname" . -}} {{- $fullName := include "garage.fullname" . -}}
{{- $svcPort := .Values.service.s3.web.port -}} {{- $svcPort := .Values.service.s3.web.port -}}
@@ -10,11 +10,11 @@ spec:
clusterIP: None clusterIP: None
ports: ports:
- port: {{ .Values.service.s3.api.port }} - port: {{ .Values.service.s3.api.port }}
targetPort: 3900 targetPort: {{ include "garage.portFromBindAddr" .Values.garage.s3.api.bindAddr | int }}
protocol: TCP protocol: TCP
name: s3-api name: s3-api
- port: {{ .Values.service.s3.web.port }} - port: {{ .Values.service.s3.web.port }}
targetPort: 3902 targetPort: {{ include "garage.portFromBindAddr" .Values.garage.s3.web.bindAddr | int }}
protocol: TCP protocol: TCP
name: s3-web name: s3-web
selector: selector:
+4 -4
View File
@@ -12,11 +12,11 @@ spec:
type: {{ .Values.service.type }} type: {{ .Values.service.type }}
ports: ports:
- port: {{ .Values.service.s3.api.port }} - port: {{ .Values.service.s3.api.port }}
targetPort: 3900 targetPort: {{ include "garage.portFromBindAddr" .Values.garage.s3.api.bindAddr | int }}
protocol: TCP protocol: TCP
name: s3-api name: s3-api
- port: {{ .Values.service.s3.web.port }} - port: {{ .Values.service.s3.web.port }}
targetPort: 3902 targetPort: {{ include "garage.portFromBindAddr" .Values.garage.s3.web.bindAddr | int }}
protocol: TCP protocol: TCP
name: s3-web name: s3-web
selector: selector:
@@ -35,8 +35,8 @@ spec:
type: ClusterIP type: ClusterIP
clusterIP: None clusterIP: None
ports: ports:
- port: 3903 - port: {{ include "garage.portFromBindAddr" .Values.garage.admin.apiBindAddr | int }}
targetPort: 3903 targetPort: {{ include "garage.portFromBindAddr" .Values.garage.admin.apiBindAddr | int }}
protocol: TCP protocol: TCP
name: metrics name: metrics
selector: selector:
@@ -28,11 +28,11 @@ spec:
scheme: {{ .Values.monitoring.metrics.serviceMonitor.scheme }} scheme: {{ .Values.monitoring.metrics.serviceMonitor.scheme }}
{{- with .Values.monitoring.metrics.serviceMonitor.tlsConfig }} {{- with .Values.monitoring.metrics.serviceMonitor.tlsConfig }}
tlsConfig: tlsConfig:
{{- toYaml . | nindent 6 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
{{- with .Values.monitoring.metrics.serviceMonitor.relabelings }} {{- with .Values.monitoring.metrics.serviceMonitor.relabelings }}
relabelings: relabelings:
{{- toYaml . | nindent 6 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
jobLabel: "{{ .Release.Name }}" jobLabel: "{{ .Release.Name }}"
selector: selector:
@@ -41,4 +41,4 @@ spec:
namespaceSelector: namespaceSelector:
matchNames: matchNames:
- {{ .Release.Namespace }} - {{ .Release.Namespace }}
{{- end }} {{- end }}
+3 -3
View File
@@ -60,11 +60,11 @@ spec:
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }} imagePullPolicy: {{ .Values.image.pullPolicy }}
ports: ports:
- containerPort: 3900 - containerPort: {{ include "garage.portFromBindAddr" .Values.garage.s3.api.bindAddr | int }}
name: s3-api name: s3-api
- containerPort: 3902 - containerPort: {{ include "garage.portFromBindAddr" .Values.garage.s3.web.bindAddr | int }}
name: web-api name: web-api
- containerPort: 3903 - containerPort: {{ include "garage.portFromBindAddr" .Values.garage.admin.apiBindAddr | int }}
name: admin name: admin
{{- with .Values.environment }} {{- with .Values.environment }}
env: env:
@@ -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
+56
View File
@@ -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
+33
View File
@@ -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
+187
View File
@@ -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
+10 -2
View File
@@ -48,11 +48,15 @@ garage:
kubernetesSkipCrd: false kubernetesSkipCrd: false
s3: s3:
api: api:
bindAddr: "[::]:3900"
region: "garage" region: "garage"
rootDomain: ".s3.garage.tld" rootDomain: ".s3.garage.tld"
web: web:
bindAddr: "[::]:3902"
rootDomain: ".web.garage.tld" rootDomain: ".web.garage.tld"
index: "index.html" index: "index.html"
admin:
apiBindAddr: "[::]:3903"
# -- Additional configuration to append to garage.toml. Use a multi-line string for custom config. # -- Additional configuration to append to garage.toml. Use a multi-line string for custom config.
# Example: # Example:
@@ -221,14 +225,14 @@ resources: {}
livenessProbe: {} livenessProbe: {}
#httpGet: #httpGet:
# path: /health # path: /health
# port: 3903 # port: 3903 # or the port from garage.admin.apiBindAddr
#initialDelaySeconds: 5 #initialDelaySeconds: 5
#periodSeconds: 30 #periodSeconds: 30
# -- Specifies a readinessProbe # -- Specifies a readinessProbe
readinessProbe: {} readinessProbe: {}
#httpGet: #httpGet:
# path: /health # path: /health
# port: 3903 # port: 3903 # or the port from garage.admin.apiBindAddr
#initialDelaySeconds: 5 #initialDelaySeconds: 5
#periodSeconds: 30 #periodSeconds: 30
@@ -242,10 +246,14 @@ affinity: {}
# See https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/ # See https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/
priorityClassName: "" priorityClassName: ""
# -- Extra container env vars, as a list of {name, value} objects (same shape
# as a Pod container's env)
environment: {} environment: {}
# -- Extra volumes, as a list of volume objects (same shape as a PodSpec's volumes)
extraVolumes: {} extraVolumes: {}
# -- Extra volume mounts, as a list of mount objects (same shape as a container's volumeMounts)
extraVolumeMounts: {} extraVolumeMounts: {}
monitoring: monitoring:
+5 -4
View File
@@ -7,6 +7,7 @@ use garage_util::time::now_msec;
use garage_model::admin_token_table::*; use garage_model::admin_token_table::*;
use garage_model::garage::Garage; use garage_model::garage::Garage;
use garage_model::permission::ExpirationTime;
use crate::api::*; use crate::api::*;
use crate::error::*; use crate::error::*;
@@ -244,8 +245,8 @@ fn admin_token_info_results(token: &AdminApiToken, now: u64) -> GetAdminTokenInf
.expect("invalid timestamp stored in db"), .expect("invalid timestamp stored in db"),
), ),
name: params.name.get().to_string(), name: params.name.get().to_string(),
expiration: params.expiration.get().map(|x| { expiration: params.expiration.get().inner().map(|x| {
DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db") DateTime::from_timestamp_millis(x.0 as i64).expect("invalid timestamp stored in db")
}), }),
expired: params.is_expired(now), expired: params.is_expired(now),
scope: params.scope.get().0.clone(), scope: params.scope.get().0.clone(),
@@ -279,10 +280,10 @@ fn apply_token_updates(
if let Some(expiration) = updates.expiration { if let Some(expiration) = updates.expiration {
params params
.expiration .expiration
.update(Some(expiration.timestamp_millis() as u64)); .update(Some(ExpirationTime(expiration.timestamp_millis() as u64)).into());
} }
if updates.never_expires { if updates.never_expires {
params.expiration.update(None); params.expiration.update(None.into());
} }
if let Some(scope) = updates.scope { if let Some(scope) = updates.scope {
params.scope.update(AdminApiTokenScope(scope)); params.scope.update(AdminApiTokenScope(scope));
+31
View File
@@ -618,6 +618,10 @@ pub enum PreviewClusterLayoutChangesResponse {
/// Plain-text information about the layout computation /// Plain-text information about the layout computation
/// (do not try to parse this) /// (do not try to parse this)
message: Vec<String>, message: Vec<String>,
/// Structured statistics about the layout computation
// FIXME for v3: remove default and skip_serializing_if
#[serde(default, skip_serializing_if = "Option::is_none")]
statistics: Option<Box<garage_rpc::layout::ComputationStat>>,
/// Details about the new cluster layout /// Details about the new cluster layout
new_layout: GetClusterLayoutResponse, new_layout: GetClusterLayoutResponse,
}, },
@@ -639,6 +643,10 @@ pub struct ApplyClusterLayoutResponse {
/// Plain-text information about the layout computation /// Plain-text information about the layout computation
/// (do not try to parse this) /// (do not try to parse this)
pub message: Vec<String>, pub message: Vec<String>,
/// Structured statistics about the layout computation
// FIXME for v3: remove default and skip_serializing_if
#[serde(default, skip_serializing_if = "Option::is_none")]
pub statistics: Option<garage_rpc::layout::ComputationStat>,
/// Details about the new cluster layout /// Details about the new cluster layout
pub layout: GetClusterLayoutResponse, pub layout: GetClusterLayoutResponse,
} }
@@ -1165,6 +1173,29 @@ pub struct LocalGetNodeInfoResponse {
pub rust_version: String, pub rust_version: String,
/// database engine used for metadata /// database engine used for metadata
pub db_engine: String, pub db_engine: String,
// FIXME for v3: remove Option<> and serde(default) for field below
// FIXME for v3: merge LocalGetNodeInfoResponse and NodeResp
/// Socket address used by other nodes to connect to this node for RPC
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = Option<String>)]
pub addr: Option<SocketAddr>,
/// Whether this node is connected in the cluster
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_up: Option<bool>,
/// Role assigned to this node in the current cluster layout
#[serde(default, skip_serializing_if = "Option::is_none")]
pub role: Option<NodeAssignedRole>,
/// Whether this node is part of an older layout version and is draining data.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub draining: Option<bool>,
/// Total and available space on the disk partition(s) containing the data
/// directory(ies)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data_partition: Option<FreeSpaceResp>,
/// Total and available space on the disk partition containing the
/// metadata directory
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata_partition: Option<FreeSpaceResp>,
} }
// ---- GetNodeStatistics ---- // ---- GetNodeStatistics ----
+58 -63
View File
@@ -90,7 +90,7 @@ impl RequestHandler for GetBucketInfoRequest {
.bucket_alias_table .bucket_alias_table
.get(&EmptyKey, &ga) .get(&EmptyKey, &ga)
.await? .await?
.and_then(|x| *x.state.get()) .and_then(|x| x.state.get().into_inner())
.ok_or_else(|| HelperError::NoSuchBucket(ga.to_string()))?, .ok_or_else(|| HelperError::NoSuchBucket(ga.to_string()))?,
(None, None, Some(search)) => { (None, None, Some(search)) => {
let helper = garage.bucket_helper(); 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 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()); return Err(CommonError::BucketAlreadyExists.into());
} }
} }
@@ -297,7 +297,7 @@ impl RequestHandler for UpdateBucketRequest {
let redirect_all = state let redirect_all = state
.website_config .website_config
.get() .get()
.as_ref() .inner()
.and_then(|wc| wc.redirect_all.clone()); .and_then(|wc| wc.redirect_all.clone());
let routing_rules = if let Some(rr) = wa.routing_rules { let routing_rules = if let Some(rr) = wa.routing_rules {
@@ -311,26 +311,29 @@ impl RequestHandler for UpdateBucketRequest {
state state
.website_config .website_config
.get() .get()
.as_ref() .inner()
.map(|wc| wc.routing_rules.clone()) .map(|wc| wc.routing_rules.clone())
.unwrap_or_default() .unwrap_or_default()
}; };
state.website_config.update(Some(WebsiteConfig { state.website_config.update(
index_document: wa.index_document.ok_or_bad_request( Some(WebsiteConfig {
"Please specify indexDocument when enabling website access.", index_document: wa.index_document.ok_or_bad_request(
)?, "Please specify indexDocument when enabling website access.",
error_document: wa.error_document, )?,
redirect_all, error_document: wa.error_document,
routing_rules, redirect_all,
})); routing_rules,
})
.into(),
);
} else { } else {
if wa.index_document.is_some() || wa.error_document.is_some() { if wa.index_document.is_some() || wa.error_document.is_some() {
return Err(Error::bad_request( return Err(Error::bad_request(
"Cannot specify indexDocument or errorDocument when disabling website access.", "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()?) 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 { 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?; garage.bucket_table.insert(&bucket).await?;
@@ -687,45 +690,36 @@ async fn bucket_info_results(
.map(|x| x.filtered_values(&garage.system.cluster_layout())) .map(|x| x.filtered_values(&garage.system.cluster_layout()))
.unwrap_or_default(); .unwrap_or_default();
let mut relevant_keys = HashMap::new(); let state = bucket.state.as_option().unwrap();
for (k, _) in bucket
.state let keys1 = state
.as_option()
.unwrap()
.authorized_keys .authorized_keys
.items() .items()
.iter() .iter()
{ .filter(|(_, p)| p.is_any())
if let Some(key) = garage .map(|(k, _)| k);
.key_table let keys2 = state
.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()
.local_aliases .local_aliases
.items() .items()
.iter() .iter()
{ .filter(|(_, _, p)| *p)
if relevant_keys.contains_key(k) { .map(|((k, _), _, _)| k);
let mut relevant_keys = HashMap::new();
for key_id in keys1.chain(keys2) {
if relevant_keys.contains_key(key_id) {
continue; continue;
} }
if let Some(key) = garage.key_table.get(&EmptyKey, k).await? { if let Some(key) = garage.key_table.get(&EmptyKey, key_id).await? {
if !key.state.is_deleted() { relevant_keys.insert(key_id.clone(), key);
relevant_keys.insert(k.clone(), key); } else {
} warn!(
"Bucket {:?} references non-existent key {}",
bucket.id, key_id
);
} }
} }
relevant_keys.retain(|_, k| !k.is_deleted());
let state = bucket.state.as_option().unwrap();
let quotas = state.quotas.get(); let quotas = state.quotas.get();
let res = GetBucketInfoResponse { let res = GetBucketInfoResponse {
@@ -739,8 +733,8 @@ async fn bucket_info_results(
.filter(|(_, _, a)| *a) .filter(|(_, _, a)| *a)
.map(|(n, _, _)| n.to_string()) .map(|(n, _, _)| n.to_string())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
website_access: state.website_config.get().is_some(), website_access: state.website_config.get().inner().is_some(),
website_config: state.website_config.get().clone().map(|wsc| { website_config: state.website_config.get().inner().cloned().map(|wsc| {
GetBucketInfoWebsiteResponse { GetBucketInfoWebsiteResponse {
index_document: wsc.index_document, index_document: wsc.index_document,
error_document: wsc.error_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 rules
.iter() .iter()
.map(xml::cors::CorsRule::from_garage_cors_rule) .map(xml::cors::CorsRule::from_garage_cors_rule)
.collect::<Vec<_>>() .collect::<Vec<_>>()
}), }),
lifecycle_rules: state.lifecycle_config.get().as_ref().map(|lc| { lifecycle_rules: state.lifecycle_config.get().inner().map(|lc| {
lc.iter() lc.iter()
.map(xml::lifecycle::LifecycleRule::from_garage_lifecycle_rule) .map(xml::lifecycle::LifecycleRule::from_garage_lifecycle_rule)
.collect::<Vec<_>>() .collect::<Vec<_>>()
}), }),
keys: relevant_keys keys: relevant_keys
.into_values() .into_values()
.filter_map(|key| { .map(|key| {
let p = key.state.as_option().unwrap(); let st = key.state.as_option().unwrap();
let permissions = p let permissions = st
.authorized_buckets .authorized_buckets
.get(&bucket.id) .get(&bucket.id)
.filter(|p| p.is_any())
.map(|p| ApiBucketKeyPerm { .map(|p| ApiBucketKeyPerm {
read: p.allow_read, read: p.allow_read,
write: p.allow_write, write: p.allow_write,
owner: p.allow_owner, 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, access_key_id: key.key_id,
name: p.name.get().to_string(), name: st.name.get().to_string(),
permissions, permissions,
bucket_local_aliases: p bucket_local_aliases,
.local_aliases }
.items()
.iter()
.filter(|(_, _, b)| *b == Some(bucket.id))
.map(|(n, _, _)| n.to_string())
.collect::<Vec<_>>(),
})
}) })
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
objects: *counters.get(OBJECTS).unwrap_or(&0), objects: *counters.get(OBJECTS).unwrap_or(&0),
+32 -26
View File
@@ -8,6 +8,7 @@ use garage_util::time::now_msec;
use garage_model::garage::Garage; use garage_model::garage::Garage;
use garage_model::key_table::*; use garage_model::key_table::*;
use garage_model::permission::ExpirationTime;
use crate::api::*; use crate::api::*;
use crate::error::*; use crate::error::*;
@@ -40,8 +41,8 @@ impl RequestHandler for ListKeysRequest {
DateTime::from_timestamp_millis(x as i64) DateTime::from_timestamp_millis(x as i64)
.expect("invalid timestamp stored in db") .expect("invalid timestamp stored in db")
}), }),
expiration: p.expiration.get().map(|x| { expiration: p.expiration.get().inner().map(|x| {
DateTime::from_timestamp_millis(x as i64) DateTime::from_timestamp_millis(x.0 as i64)
.expect("invalid timestamp stored in db") .expect("invalid timestamp stored in db")
}), }),
expired: p.is_expired(now), expired: p.is_expired(now),
@@ -187,38 +188,42 @@ async fn key_info_results(
key: Key, key: Key,
show_secret: bool, show_secret: bool,
) -> Result<GetKeyInfoResponse, Error> { ) -> Result<GetKeyInfoResponse, Error> {
let mut relevant_buckets = HashMap::new();
let key_state = key.state.as_option().unwrap(); let key_state = key.state.as_option().unwrap();
for id in key_state let buckets1 = key_state
.authorized_buckets .authorized_buckets
.items() .items()
.iter() .iter()
.map(|(id, _)| id) .filter(|(_, p)| p.is_any())
.chain( .map(|(id, _)| id);
key_state let buckets2 = key_state
.local_aliases .local_aliases
.items() .items()
.iter() .iter()
.filter_map(|(_, _, v)| v.as_ref()), .filter_map(|(_, _, v)| v.inner());
) {
if !relevant_buckets.contains_key(id) { let mut relevant_buckets = HashMap::new();
if let Some(b) = garage.bucket_table.get(&EmptyKey, id).await? { for bucket_id in buckets1.chain(buckets2) {
if b.state.as_option().is_some() { if !relevant_buckets.contains_key(bucket_id) {
relevant_buckets.insert(*id, b); 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 { let res = GetKeyInfoResponse {
name: key_state.name.get().clone(), name: key_state.name.get().clone(),
created: key_state.created.map(|x| { created: key_state.created.map(|x| {
DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db") DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db")
}), }),
expiration: key_state.expiration.get().map(|x| { expiration: key_state.expiration.get().inner().map(|x| {
DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db") DateTime::from_timestamp_millis(x.0 as i64).expect("invalid timestamp stored in db")
}), }),
expired: key_state.is_expired(now_msec()), expired: key_state.is_expired(now_msec()),
access_key_id: key.key_id.clone(), access_key_id: key.key_id.clone(),
@@ -232,7 +237,7 @@ async fn key_info_results(
}, },
buckets: relevant_buckets buckets: relevant_buckets
.into_values() .into_values()
.filter_map(|bucket| { .map(|bucket| {
let state = bucket.state.as_option().unwrap(); let state = bucket.state.as_option().unwrap();
let permissions = key_state let permissions = key_state
.authorized_buckets .authorized_buckets
@@ -242,8 +247,9 @@ async fn key_info_results(
read: p.allow_read, read: p.allow_read,
write: p.allow_write, write: p.allow_write,
owner: p.allow_owner, owner: p.allow_owner,
})?; })
Some(KeyInfoBucketResponse { .unwrap_or_default();
KeyInfoBucketResponse {
id: hex::encode(bucket.id), id: hex::encode(bucket.id),
global_aliases: state global_aliases: state
.aliases .aliases
@@ -260,7 +266,7 @@ async fn key_info_results(
.map(|((_, n), _, _)| n.to_string()) .map(|((_, n), _, _)| n.to_string())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
permissions, permissions,
}) }
}) })
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
}; };
@@ -283,10 +289,10 @@ fn apply_key_updates(key: &mut Key, updates: UpdateKeyRequestBody) -> Result<(),
if let Some(expiration) = updates.expiration { if let Some(expiration) = updates.expiration {
key_state key_state
.expiration .expiration
.update(Some(expiration.timestamp_millis() as u64)); .update(Some(ExpirationTime(expiration.timestamp_millis() as u64)).into());
} }
if updates.never_expires { if updates.never_expires {
key_state.expiration.update(None); key_state.expiration.update(None.into());
} }
if let Some(allow) = updates.allow { if let Some(allow) = updates.allow {
if allow.create_bucket { if allow.create_bucket {
+13 -7
View File
@@ -53,7 +53,7 @@ fn format_cluster_layout(layout: &layout::LayoutHistory) -> GetClusterLayoutResp
.roles .roles
.items() .items()
.iter() .iter()
.filter(|(k, _, v)| current.roles.get(k) != Some(v)) .filter(|(k, _, v)| current.roles.get(k).and_then(|vv| vv.0.as_ref()) != v.0.as_ref())
.map(|(k, _, v)| match &v.0 { .map(|(k, _, v)| match &v.0 {
None => NodeRoleChange { None => NodeRoleChange {
id: hex::encode(k), id: hex::encode(k),
@@ -255,10 +255,14 @@ impl RequestHandler for PreviewClusterLayoutChangesRequest {
Ok(PreviewClusterLayoutChangesResponse::Error { error }) Ok(PreviewClusterLayoutChangesResponse::Error { error })
} }
Err(e) => Err(e.into()), Err(e) => Err(e.into()),
Ok((new_layout, msg)) => Ok(PreviewClusterLayoutChangesResponse::Success { Ok((new_layout, stat)) => {
message: msg, let message = stat.to_message();
new_layout: format_cluster_layout(&new_layout), Ok(PreviewClusterLayoutChangesResponse::Success {
}), message,
statistics: Some(Box::new(stat)),
new_layout: format_cluster_layout(&new_layout),
})
}
} }
} }
} }
@@ -272,7 +276,8 @@ impl RequestHandler for ApplyClusterLayoutRequest {
_admin: &Admin, _admin: &Admin,
) -> Result<ApplyClusterLayoutResponse, Error> { ) -> Result<ApplyClusterLayoutResponse, Error> {
let layout = garage.system.cluster_layout().inner().clone(); let layout = garage.system.cluster_layout().inner().clone();
let (layout, msg) = layout.apply_staged_changes(self.version)?; let (layout, stat) = layout.apply_staged_changes(self.version)?;
let message = stat.to_message();
garage garage
.system .system
@@ -281,7 +286,8 @@ impl RequestHandler for ApplyClusterLayoutRequest {
.await?; .await?;
Ok(ApplyClusterLayoutResponse { Ok(ApplyClusterLayoutResponse {
message: msg, message,
statistics: Some(stat),
layout: format_cluster_layout(&layout), layout: format_cluster_layout(&layout),
}) })
} }
+38
View File
@@ -25,6 +25,9 @@ impl RequestHandler for LocalGetNodeInfoRequest {
let sys_status = garage.system.local_status(); let sys_status = garage.system.local_status();
let hostname = sys_status.hostname.unwrap_or_default().to_string(); let hostname = sys_status.hostname.unwrap_or_default().to_string();
let layout = garage.system.cluster_layout();
let current_layout = layout.inner().current();
Ok(LocalGetNodeInfoResponse { Ok(LocalGetNodeInfoResponse {
node_id: hex::encode(garage.system.id), node_id: hex::encode(garage.system.id),
hostname: Some(hostname), hostname: Some(hostname),
@@ -33,6 +36,41 @@ impl RequestHandler for LocalGetNodeInfoRequest {
.map(|features| features.iter().map(ToString::to_string).collect()), .map(|features| features.iter().map(ToString::to_string).collect()),
rust_version: garage_util::version::rust_version().to_string(), rust_version: garage_util::version::rust_version().to_string(),
db_engine: garage.db.engine(), db_engine: garage.db.engine(),
is_up: Some(true),
addr: garage
.system
.get_known_nodes()
.iter()
.find(|x| x.id == garage.system.id)
.and_then(|x| x.addr),
draining: Some(
current_layout.node_role(&garage.system.id).is_none()
&& layout
.inner()
.versions
.iter()
.filter(|x| x.version != current_layout.version)
.any(|x| x.node_role(&garage.system.id).is_some()),
),
role: current_layout
.node_role(&garage.system.id)
.map(|v| NodeAssignedRole {
zone: v.zone.clone(),
capacity: v.capacity,
tags: v.tags.clone(),
}),
data_partition: sys_status
.data_disk_avail
.map(|(avail, total)| FreeSpaceResp {
available: avail,
total,
}),
metadata_partition: sys_status
.meta_disk_avail
.map(|(avail, total)| FreeSpaceResp {
available: avail,
total,
}),
}) })
} }
} }
+1 -8
View File
@@ -360,14 +360,7 @@ impl Worker for BlockRcRepair {
_must_exit: &mut watch::Receiver<bool>, _must_exit: &mut watch::Receiver<bool>,
) -> Result<WorkerState, GarageError> { ) -> Result<WorkerState, GarageError> {
for _i in 0..RC_REPAIR_ITER_COUNT { for _i in 0..RC_REPAIR_ITER_COUNT {
let next1 = self let next1 = self.block_manager.rc.get_first_hash_from(self.cursor)?;
.block_manager
.rc
.rc_table
.range(self.cursor.as_slice()..)?
.next()
.transpose()?
.map(|(k, _)| Hash::try_from(k.as_slice()).unwrap());
let next2 = self let next2 = self
.block_ref_table .block_ref_table
.data .data
+1 -1
View File
@@ -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_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 { match bucket_website_config {
Some(_v) => Ok(true), Some(_v) => Ok(true),
+139 -14
View File
@@ -1,8 +1,9 @@
use std::sync::Arc; use std::sync::Arc;
use http::header::{ use http::header::{
ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue, ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS,
ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_REQUEST_HEADERS,
ACCESS_CONTROL_REQUEST_METHOD, VARY,
}; };
use hyper::{body::Body, body::Incoming as IncomingBody, Request, Response, StatusCode}; use hyper::{body::Body, body::Incoming as IncomingBody, Request, Response, StatusCode};
@@ -12,20 +13,25 @@ use garage_model::garage::Garage;
use crate::common_error::{CommonError, OkOrBadRequest, OkOrInternalError}; use crate::common_error::{CommonError, OkOrBadRequest, OkOrInternalError};
use crate::helpers::*; use crate::helpers::*;
// Return both the matching rule and the parsed Origin header so callers that
// apply CORS headers don't have to repeat Origin lookup and validation.
pub fn find_matching_cors_rule<'a, B>( pub fn find_matching_cors_rule<'a, B>(
bucket_params: &'a BucketParams, bucket_params: &'a BucketParams,
req: &Request<B>, req: &'a Request<B>,
) -> Result<Option<&'a GarageCorsRule>, CommonError> { ) -> 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") { if let Some(origin) = req.headers().get("Origin") {
let origin = origin.to_str()?; let origin = origin.to_str()?;
let request_headers = match req.headers().get(ACCESS_CONTROL_REQUEST_HEADERS) { let request_headers = match req.headers().get(ACCESS_CONTROL_REQUEST_HEADERS) {
Some(h) => h.to_str()?.split(',').map(|h| h.trim()).collect::<Vec<_>>(), Some(h) => h.to_str()?.split(',').map(|h| h.trim()).collect::<Vec<_>>(),
None => vec![], None => vec![],
}; };
return Ok(cors_config.iter().find(|rule| { return Ok(cors_config
cors_rule_matches(rule, origin, req.method().as_ref(), request_headers.iter()) .iter()
})); .find(|rule| {
cors_rule_matches(rule, origin, req.method().as_ref(), request_headers.iter())
})
.map(|rule| (rule, origin)));
} }
} }
Ok(None) Ok(None)
@@ -53,12 +59,16 @@ where
pub fn add_cors_headers( pub fn add_cors_headers(
resp: &mut Response<impl Body>, resp: &mut Response<impl Body>,
rule: &GarageCorsRule, rule: &GarageCorsRule,
request_origin: &str,
) -> Result<(), http::header::InvalidHeaderValue> { ) -> Result<(), http::header::InvalidHeaderValue> {
let h = resp.headers_mut(); let h = resp.headers_mut();
h.insert( let is_wildcard_origin = rule.allow_origins.iter().any(|origin| origin == "*");
ACCESS_CONTROL_ALLOW_ORIGIN, let allow_origin = if is_wildcard_origin {
rule.allow_origins.join(", ").parse()?, "*"
); } else {
request_origin
};
h.insert(ACCESS_CONTROL_ALLOW_ORIGIN, allow_origin.parse()?);
h.insert( h.insert(
ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_METHODS,
rule.allow_methods.join(", ").parse()?, rule.allow_methods.join(", ").parse()?,
@@ -71,6 +81,12 @@ pub fn add_cors_headers(
ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_EXPOSE_HEADERS,
rule.expose_headers.join(", ").parse()?, rule.expose_headers.join(", ").parse()?,
); );
// When ACAO reflects the request origin instead of returning "*",
// caches must vary on the Origin request header to avoid reusing
// a response generated for one origin when serving another origin.
if !is_wildcard_origin {
h.insert(VARY, HeaderValue::from_static("Origin"));
}
Ok(()) Ok(())
} }
@@ -107,6 +123,7 @@ pub fn handle_options_api(
Ok(Response::builder() Ok(Response::builder()
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
.header(ACCESS_CONTROL_ALLOW_METHODS, "*") .header(ACCESS_CONTROL_ALLOW_METHODS, "*")
.header(ACCESS_CONTROL_ALLOW_HEADERS, "*")
.status(StatusCode::OK) .status(StatusCode::OK)
.body(EmptyBody::new())?) .body(EmptyBody::new())?)
} }
@@ -141,7 +158,7 @@ pub fn handle_options_for_bucket<B>(
None => vec![], 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 let matching_rule = cors_config
.iter() .iter()
.find(|rule| cors_rule_matches(rule, origin, request_method, request_headers.iter())); .find(|rule| cors_rule_matches(rule, origin, request_method, request_headers.iter()));
@@ -149,7 +166,17 @@ pub fn handle_options_for_bucket<B>(
let mut resp = Response::builder() let mut resp = Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
.body(EmptyBody::new())?; .body(EmptyBody::new())?;
add_cors_headers(&mut resp, rule).ok_or_internal_error("Invalid CORS configuration")?; add_cors_headers(&mut resp, rule, origin)
.ok_or_internal_error("Invalid CORS configuration")?;
// Preflight responses vary not only on Origin but also on the
// requested method and requested headers, so caches must not
// reuse one preflight decision for a different preflight input.
resp.headers_mut().insert(
VARY,
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
.parse()
.expect("static vary header"),
);
return Ok(resp); return Ok(resp);
} }
} }
@@ -158,3 +185,101 @@ pub fn handle_options_for_bucket<B>(
"This CORS request is not allowed.".into(), "This CORS request is not allowed.".into(),
)) ))
} }
#[cfg(test)]
mod tests {
use super::*;
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![],
}])
.into(),
);
bucket_params
}
fn preflight_request(origin: &str) -> Request<()> {
Request::builder()
.method("OPTIONS")
.uri("http://example.test/bucket")
.header("Origin", origin)
.header(ACCESS_CONTROL_REQUEST_METHOD, "PUT")
.body(())
.unwrap()
}
#[test]
fn preflight_with_single_allowed_origin_returns_request_origin() {
let bucket_params = bucket_params_with_rule(vec!["https://app.example.test"]);
let req = preflight_request("https://app.example.test");
let resp = handle_options_for_bucket(&req, &bucket_params).unwrap();
assert_eq!(
resp.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN).unwrap(),
"https://app.example.test"
);
let vary_values: Vec<_> = resp
.headers()
.get_all(VARY)
.iter()
.map(|value| value.to_str().unwrap())
.collect();
assert_eq!(
vary_values,
vec!["Origin, Access-Control-Request-Method, Access-Control-Request-Headers",]
);
}
#[test]
fn preflight_with_multiple_allowed_origins_reflects_request_origin() {
let bucket_params = bucket_params_with_rule(vec![
"https://app.example.test",
"https://admin.example.test",
]);
let req = preflight_request("https://app.example.test");
let resp = handle_options_for_bucket(&req, &bucket_params).unwrap();
// This assertion documents the behavior browsers expect:
// even if multiple origins are allowed by configuration, the
// response should reflect the request origin rather than emit
// a comma-separated list. It currently fails and is meant to
// turn green once header generation is corrected.
assert_eq!(
resp.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN).unwrap(),
"https://app.example.test"
);
}
#[test]
fn preflight_with_wildcard_allowed_origin_returns_wildcard() {
let bucket_params = bucket_params_with_rule(vec!["*"]);
let req = preflight_request("https://app.example.test");
let resp = handle_options_for_bucket(&req, &bucket_params).unwrap();
assert_eq!(
resp.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN).unwrap(),
"*"
);
let vary_values: Vec<_> = resp
.headers()
.get_all(VARY)
.iter()
.map(|value| value.to_str().unwrap())
.collect();
assert_eq!(
vary_values,
vec!["Origin, Access-Control-Request-Method, Access-Control-Request-Headers",]
);
}
}
+12 -2
View File
@@ -357,7 +357,13 @@ pub fn canonical_request(
items.join("&") items.join("&")
}; };
// Canonical header string calculated from signed headers // Canonical header string calculated from signed headers.
//
// Per the SigV4 spec, signed header values must have sequential
// internal whitespace collapsed to a single space, in addition to
// being trimmed. AWS SDKs do this before computing the signature
// but transmit the raw value on the wire, so we must match.
// -> https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html
let canonical_header_string = signed_headers let canonical_header_string = signed_headers
.iter() .iter()
.map(|name| { .map(|name| {
@@ -372,7 +378,11 @@ pub fn canonical_request(
built_string.push(','); built_string.push(',');
built_string.push_str(extend_string); built_string.push_str(extend_string);
} }
Ok(format!("{}:{}", name.as_str(), built_string.trim())) let normalized = built_string
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
Ok(format!("{}:{}", name.as_str(), normalized))
}) })
.collect::<Result<Vec<String>, Error>>()? .collect::<Result<Vec<String>, Error>>()?
.join("\n"); .join("\n");
+3 -3
View File
@@ -111,7 +111,7 @@ impl ApiHandler for K2VApiServer {
Method::GET | Method::HEAD | Method::POST => { Method::GET | Method::HEAD | Method::POST => {
find_matching_cors_rule(&bucket_params, &req) find_matching_cors_rule(&bucket_params, &req)
.ok_or_internal_error("Error looking up CORS rule")? .ok_or_internal_error("Error looking up CORS rule")?
.cloned() .map(|(rule, origin)| (rule.clone(), origin.to_string()))
} }
_ => None, _ => None,
}; };
@@ -164,8 +164,8 @@ impl ApiHandler for K2VApiServer {
// If request was a success and we have a CORS rule that applies to it, // If request was a success and we have a CORS rule that applies to it,
// add the corresponding CORS headers to the response // add the corresponding CORS headers to the response
let mut resp_ok = resp?; let mut resp_ok = resp?;
if let Some(rule) = matching_cors_rule { if let Some((rule, origin)) = matching_cors_rule {
add_cors_headers(&mut resp_ok, &rule) add_cors_headers(&mut resp_ok, &rule, &origin)
.ok_or_internal_error("Invalid bucket CORS configuration")?; .ok_or_internal_error("Invalid bucket CORS configuration")?;
} }
+16 -8
View File
@@ -5,12 +5,13 @@ use serde::{Deserialize, Serialize};
use garage_table::{EnumerationOrder, TableSchema}; use garage_table::{EnumerationOrder, TableSchema};
use garage_model::k2v::item_table::*; use garage_model::k2v::item_table::*;
use garage_model::k2v::rpc::K2VMonotonicRead;
use garage_api_common::helpers::*; use garage_api_common::helpers::*;
use crate::api_server::{ReqBody, ResBody}; use crate::api_server::{ReqBody, ResBody};
use crate::error::*; use crate::error::*;
use crate::item::parse_causality_token; use crate::item::{is_monotonic_read, parse_causality_token};
use crate::range::read_range; use crate::range::read_range;
pub async fn handle_insert_batch( pub async fn handle_insert_batch(
@@ -47,12 +48,13 @@ pub async fn handle_read_batch(
ctx: ReqCtx, ctx: ReqCtx,
req: Request<ReqBody>, req: Request<ReqBody>,
) -> Result<Response<ResBody>, Error> { ) -> Result<Response<ResBody>, Error> {
let monotonic_read = is_monotonic_read(&req)?;
let queries = req.into_body().json::<Vec<ReadBatchQuery>>().await?; let queries = req.into_body().json::<Vec<ReadBatchQuery>>().await?;
let resp_results = futures::future::join_all( let resp_results = futures::future::join_all(
queries queries
.into_iter() .into_iter()
.map(|q| handle_read_batch_query(&ctx, q)), .map(|q| handle_read_batch_query(&ctx, q, monotonic_read)),
) )
.await; .await;
@@ -67,6 +69,7 @@ pub async fn handle_read_batch(
async fn handle_read_batch_query( async fn handle_read_batch_query(
ctx: &ReqCtx, ctx: &ReqCtx,
query: ReadBatchQuery, query: ReadBatchQuery,
monotonic_read: K2VMonotonicRead,
) -> Result<ReadBatchResponse, Error> { ) -> Result<ReadBatchResponse, Error> {
let ReqCtx { let ReqCtx {
garage, bucket_id, .. garage, bucket_id, ..
@@ -90,12 +93,13 @@ async fn handle_read_batch_query(
.start .start
.as_ref() .as_ref()
.ok_or_bad_request("start should be specified if single_item is set")?; .ok_or_bad_request("start should be specified if single_item is set")?;
let item = garage let item = match monotonic_read {
.k2v K2VMonotonicRead::Monotonic => {
.item_table garage.k2v.item_table.get_monotonic(&partition, sk).await?
.get(&partition, sk) }
.await? K2VMonotonicRead::NonMonotonic => garage.k2v.item_table.get(&partition, sk).await?,
.filter(|e| K2VItemTable::matches_filter(e, &filter)); }
.filter(|e| K2VItemTable::matches_filter(e, &filter));
match item { match item {
Some(i) => (vec![ReadBatchResponseItem::from(i)], false, None), Some(i) => (vec![ReadBatchResponseItem::from(i)], false, None),
None => (vec![], false, None), None => (vec![], false, None),
@@ -110,6 +114,7 @@ async fn handle_read_batch_query(
query.limit, query.limit,
Some(filter), Some(filter),
EnumerationOrder::from_reverse(query.reverse), EnumerationOrder::from_reverse(query.reverse),
monotonic_read,
) )
.await?; .await?;
@@ -218,6 +223,7 @@ async fn handle_delete_batch_query(
None, None,
Some(filter), Some(filter),
EnumerationOrder::Forward, EnumerationOrder::Forward,
K2VMonotonicRead::NonMonotonic,
) )
.await?; .await?;
assert!(!more); assert!(!more);
@@ -260,6 +266,7 @@ pub(crate) async fn handle_poll_range(
let ReqCtx { let ReqCtx {
garage, bucket_id, .. garage, bucket_id, ..
} = ctx; } = ctx;
let monotonic_read = is_monotonic_read(&req)?;
use garage_model::k2v::sub::PollRange; use garage_model::k2v::sub::PollRange;
let query = req.into_body().json::<PollRangeQuery>().await?; let query = req.into_body().json::<PollRangeQuery>().await?;
@@ -281,6 +288,7 @@ pub(crate) async fn handle_poll_range(
}, },
query.seen_marker, query.seen_marker,
timeout_msec, timeout_msec,
monotonic_read,
) )
.await .await
.map_err(pass_helper_error)?; .map_err(pass_helper_error)?;
+7 -1
View File
@@ -44,6 +44,10 @@ pub enum Error {
#[error("Invalid causality token")] #[error("Invalid causality token")]
InvalidCausalityToken, 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) /// The client asked for an invalid return format (invalid Accept header)
#[error("Not acceptable: {0}")] #[error("Not acceptable: {0}")]
NotAcceptable(String), NotAcceptable(String),
@@ -85,6 +89,7 @@ impl Error {
Error::InvalidBase64(_) => "InvalidBase64", Error::InvalidBase64(_) => "InvalidBase64",
Error::InvalidUtf8Str(_) => "InvalidUtf8String", Error::InvalidUtf8Str(_) => "InvalidUtf8String",
Error::InvalidCausalityToken => "CausalityToken", Error::InvalidCausalityToken => "CausalityToken",
Error::InvalidNonMonotonicRead(_) => "InvalidNonMonotonicRead",
Error::InvalidDigest(_) => "InvalidDigest", Error::InvalidDigest(_) => "InvalidDigest",
} }
} }
@@ -101,7 +106,8 @@ impl ApiError for Error {
| Error::InvalidBase64(_) | Error::InvalidBase64(_)
| Error::InvalidUtf8Str(_) | Error::InvalidUtf8Str(_)
| Error::InvalidDigest(_) | Error::InvalidDigest(_)
| Error::InvalidCausalityToken => StatusCode::BAD_REQUEST, | Error::InvalidCausalityToken
| Error::InvalidNonMonotonicRead(_) => StatusCode::BAD_REQUEST,
} }
} }
+2
View File
@@ -4,6 +4,7 @@ use serde::Serialize;
use garage_table::util::*; use garage_table::util::*;
use garage_model::k2v::item_table::{BYTES, CONFLICTS, ENTRIES, VALUES}; use garage_model::k2v::item_table::{BYTES, CONFLICTS, ENTRIES, VALUES};
use garage_model::k2v::rpc::K2VMonotonicRead;
use garage_api_common::helpers::*; use garage_api_common::helpers::*;
@@ -40,6 +41,7 @@ pub async fn handle_read_index(
limit, limit,
Some((DeletedFilter::NotDeleted, node_id_vec)), Some((DeletedFilter::NotDeleted, node_id_vec)),
EnumerationOrder::from_reverse(reverse), EnumerationOrder::from_reverse(reverse),
K2VMonotonicRead::NonMonotonic,
) )
.await?; .await?;
+37 -13
View File
@@ -5,6 +5,7 @@ use hyper::{Request, Response, StatusCode};
use garage_model::k2v::causality::*; use garage_model::k2v::causality::*;
use garage_model::k2v::item_table::*; use garage_model::k2v::item_table::*;
use garage_model::k2v::rpc::K2VMonotonicRead;
use garage_api_common::helpers::*; use garage_api_common::helpers::*;
@@ -12,6 +13,7 @@ use crate::api_server::{ReqBody, ResBody};
use crate::error::*; use crate::error::*;
pub const X_GARAGE_CAUSALITY_TOKEN: &str = "X-Garage-Causality-Token"; 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 { pub enum ReturnFormat {
Json, Json,
@@ -23,6 +25,21 @@ pub(crate) fn parse_causality_token(s: &str) -> Result<CausalContext, Error> {
CausalContext::parse(s).ok_or(Error::InvalidCausalityToken) 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 { impl ReturnFormat {
pub fn from(req: &Request<ReqBody>) -> Result<Self, Error> { pub fn from(req: &Request<ReqBody>) -> Result<Self, Error> {
let accept = match req.headers().get(header::ACCEPT) { let accept = match req.headers().get(header::ACCEPT) {
@@ -108,21 +125,26 @@ pub async fn handle_read_item(
let ReqCtx { let ReqCtx {
garage, bucket_id, .. garage, bucket_id, ..
} = &ctx; } = &ctx;
let monotonic_read = is_monotonic_read(req)?;
let format = ReturnFormat::from(req)?; let format = ReturnFormat::from(req)?;
let partition_key = K2VItemPartition {
bucket_id: *bucket_id,
partition_key: partition_key.to_string(),
};
let item = garage let item = match monotonic_read {
.k2v K2VMonotonicRead::Monotonic => {
.item_table garage
.get( .k2v
&K2VItemPartition { .item_table
bucket_id: *bucket_id, .get_monotonic(&partition_key, sort_key)
partition_key: partition_key.to_string(), .await?
}, }
sort_key, K2VMonotonicRead::NonMonotonic => {
) garage.k2v.item_table.get(&partition_key, sort_key).await?
.await? }
.ok_or(Error::NoSuchKey)?; }
.ok_or(Error::NoSuchKey)?;
format.make_response(&item) format.make_response(&item)
} }
@@ -214,6 +236,7 @@ pub async fn handle_poll_item(
let ReqCtx { let ReqCtx {
garage, bucket_id, .. garage, bucket_id, ..
} = &ctx; } = &ctx;
let monotonic_read = is_monotonic_read(req)?;
let format = ReturnFormat::from(req)?; let format = ReturnFormat::from(req)?;
let causal_context = let causal_context =
@@ -230,6 +253,7 @@ pub async fn handle_poll_item(
sort_key, sort_key,
causal_context, causal_context,
timeout_msec, timeout_msec,
monotonic_read,
) )
.await?; .await?;
+27 -9
View File
@@ -4,6 +4,8 @@
use std::sync::Arc; use std::sync::Arc;
use garage_model::k2v::rpc::K2VMonotonicRead;
use garage_table::replication::TableShardedReplication; use garage_table::replication::TableShardedReplication;
use garage_table::*; use garage_table::*;
@@ -23,6 +25,7 @@ pub(crate) async fn read_range<F>(
limit: Option<u64>, limit: Option<u64>,
filter: Option<F::Filter>, filter: Option<F::Filter>,
enumeration_order: EnumerationOrder, enumeration_order: EnumerationOrder,
monotonic_read: K2VMonotonicRead,
) -> Result<(Vec<F::E>, bool, Option<String>), Error> ) -> Result<(Vec<F::E>, bool, Option<String>), Error>
where where
F: TableSchema<S = String> + 'static, F: TableSchema<S = String> + 'static,
@@ -53,15 +56,30 @@ where
1000, 1000,
limit.map(|x| x as usize).unwrap_or(usize::MAX - 10) - entries.len() + 2, limit.map(|x| x as usize).unwrap_or(usize::MAX - 10) - entries.len() + 2,
); );
let get_ret = table let get_ret = match monotonic_read {
.get_range( K2VMonotonicRead::Monotonic => {
partition_key, table
start.clone(), .get_range_monotonic(
filter.clone(), partition_key,
n_get, start.clone(),
enumeration_order, filter.clone(),
) n_get,
.await?; 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(); let get_ret_len = get_ret.len();
+4 -3
View File
@@ -159,7 +159,8 @@ impl ApiHandler for S3ApiServer {
return Err(Error::forbidden("Operation is not allowed for this key.")); return Err(Error::forbidden("Operation is not allowed for this key."));
} }
let matching_cors_rule = find_matching_cors_rule(&bucket_params, &req)?.cloned(); let matching_cors = find_matching_cors_rule(&bucket_params, &req)?
.map(|(rule, origin)| (rule.clone(), origin.to_string()));
let ctx = ReqCtx { let ctx = ReqCtx {
garage, garage,
@@ -334,8 +335,8 @@ impl ApiHandler for S3ApiServer {
// If request was a success and we have a CORS rule that applies to it, // If request was a success and we have a CORS rule that applies to it,
// add the corresponding CORS headers to the response // add the corresponding CORS headers to the response
let mut resp_ok = resp?; let mut resp_ok = resp?;
if let Some(rule) = matching_cors_rule { if let Some((rule, origin)) = matching_cors {
add_cors_headers(&mut resp_ok, &rule) add_cors_headers(&mut resp_ok, &rule, &origin)
.ok_or_internal_error("Invalid bucket CORS configuration")?; .ok_or_internal_error("Invalid bucket CORS configuration")?;
} }
+6 -3
View File
@@ -122,7 +122,7 @@ pub async fn handle_list_buckets(
for (alias, _, _active) in bucket.aliases().iter().filter(|(_, _, active)| *active) { for (alias, _, _active) in bucket.aliases().iter().filter(|(_, _, active)| *active) {
let alias_opt = garage.bucket_alias_table.get(&EmptyKey, alias).await?; let alias_opt = garage.bucket_alias_table.get(&EmptyKey, alias).await?;
if let Some(alias_ent) = alias_opt { 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); 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() { 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); 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 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. // If the bucket has no other aliases, this is a true deletion.
// Otherwise, it is just an alias removal. // Otherwise, it is just an alias removal.
+3 -3
View File
@@ -13,7 +13,7 @@ use crate::xml::to_xml_with_header;
pub async fn handle_get_cors(ctx: ReqCtx) -> Result<Response<ResBody>, Error> { pub async fn handle_get_cors(ctx: ReqCtx) -> Result<Response<ResBody>, Error> {
let ReqCtx { bucket_params, .. } = ctx; 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 { let wc = CorsConfiguration {
xmlns: (), xmlns: (),
cors_rules: cors cors_rules: cors
@@ -38,7 +38,7 @@ pub async fn handle_delete_cors(ctx: ReqCtx) -> Result<Response<ResBody>, Error>
mut bucket_params, mut bucket_params,
.. ..
} = ctx; } = ctx;
bucket_params.cors_config.update(None); bucket_params.cors_config.update(None.into());
garage garage
.bucket_table .bucket_table
.insert(&Bucket::present(bucket_id, bucket_params)) .insert(&Bucket::present(bucket_id, bucket_params))
@@ -67,7 +67,7 @@ pub async fn handle_put_cors(
bucket_params bucket_params
.cors_config .cors_config
.update(Some(conf.into_garage_cors_config()?)); .update(Some(conf.into_garage_cors_config()?).into());
garage garage
.bucket_table .bucket_table
.insert(&Bucket::present(bucket_id, bucket_params)) .insert(&Bucket::present(bucket_id, bucket_params))
+15 -2
View File
@@ -83,8 +83,21 @@ pub async fn handle_delete_objects(
} }
ret_deleted.push(s3_xml::Deleted { ret_deleted.push(s3_xml::Deleted {
key: s3_xml::Value(obj.key.clone()), key: s3_xml::Value(obj.key.clone()),
version_id: s3_xml::Value(hex::encode(deleted_version)), version_id: Some(s3_xml::Value(hex::encode(deleted_version))),
delete_marker_version_id: s3_xml::Value(hex::encode(delete_marker_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) => { Err(e) => {
+3 -3
View File
@@ -14,7 +14,7 @@ use garage_model::bucket_table::Bucket;
pub async fn handle_get_lifecycle(ctx: ReqCtx) -> Result<Response<ResBody>, Error> { pub async fn handle_get_lifecycle(ctx: ReqCtx) -> Result<Response<ResBody>, Error> {
let ReqCtx { bucket_params, .. } = ctx; 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 wc = LifecycleConfiguration::from_garage_lifecycle_config(lifecycle);
let xml = to_xml_with_header(&wc)?; let xml = to_xml_with_header(&wc)?;
Ok(Response::builder() Ok(Response::builder()
@@ -33,7 +33,7 @@ pub async fn handle_delete_lifecycle(ctx: ReqCtx) -> Result<Response<ResBody>, E
mut bucket_params, mut bucket_params,
.. ..
} = ctx; } = ctx;
bucket_params.lifecycle_config.update(None); bucket_params.lifecycle_config.update(None.into());
garage garage
.bucket_table .bucket_table
.insert(&Bucket::present(bucket_id, bucket_params)) .insert(&Bucket::present(bucket_id, bucket_params))
@@ -62,7 +62,7 @@ pub async fn handle_put_lifecycle(
.validate_into_garage_lifecycle_config() .validate_into_garage_lifecycle_config()
.ok_or_bad_request("Invalid lifecycle configuration")?; .ok_or_bad_request("Invalid lifecycle configuration")?;
bucket_params.lifecycle_config.update(Some(config)); bucket_params.lifecycle_config.update(Some(config).into());
garage garage
.bucket_table .bucket_table
.insert(&Bucket::present(bucket_id, bucket_params)) .insert(&Bucket::present(bucket_id, bucket_params))
+13 -13
View File
@@ -83,10 +83,12 @@ pub async fn handle_post_object(
}; };
// Current part is file. Do some checks before handling to PutObject code // Current part is file. Do some checks before handling to PutObject code
let key = params let key = std::str::from_utf8(
.get("key") params
.ok_or_bad_request("No key was provided")? .get("key")
.to_str()?; .ok_or_bad_request("No key was provided")?
.as_bytes(),
)?;
let policy = params let policy = params
.get("policy") .get("policy")
.ok_or_bad_request("No policy was provided")? .ok_or_bad_request("No policy was provided")?
@@ -121,7 +123,7 @@ pub async fn handle_post_object(
&bucket_params, &bucket_params,
&Request::from_parts(head.clone(), empty_body::<Infallible>()), &Request::from_parts(head.clone(), empty_body::<Infallible>()),
)? )?
.cloned(); .map(|(rule, origin)| (rule.clone(), origin.to_string()));
let decoded_policy = BASE64_STANDARD let decoded_policy = BASE64_STANDARD
.decode(policy) .decode(policy)
@@ -351,8 +353,8 @@ pub async fn handle_post_object(
} }
}; };
if let Some(rule) = matching_cors_rule { if let Some((rule, origin)) = matching_cors_rule {
add_cors_headers(&mut resp, &rule) add_cors_headers(&mut resp, &rule, &origin)
.ok_or_internal_error("Invalid bucket CORS configuration")?; .ok_or_internal_error("Invalid bucket CORS configuration")?;
} }
@@ -473,12 +475,10 @@ where
)))); ))));
} }
} }
Poll::Ready(None) => { Poll::Ready(None) if !self.length.contains(&self.read) => {
if !self.length.contains(&self.read) { return Poll::Ready(Some(Err(Error::bad_request(
return Poll::Ready(Some(Err(Error::bad_request( "File size does not match policy",
"File size does not match policy", ))));
))));
}
} }
_ => {} _ => {}
} }
+4 -1
View File
@@ -679,7 +679,10 @@ pub(crate) fn extract_metadata_headers(
]; ];
for name in standard_header.iter() { for name in standard_header.iter() {
if let Some(value) = headers.get(name) { 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(),
));
} }
} }
+3 -3
View File
@@ -16,7 +16,7 @@ pub const X_AMZ_WEBSITE_REDIRECT_LOCATION: HeaderName =
pub async fn handle_get_website(ctx: ReqCtx) -> Result<Response<ResBody>, Error> { pub async fn handle_get_website(ctx: ReqCtx) -> Result<Response<ResBody>, Error> {
let ReqCtx { bucket_params, .. } = ctx; 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 { let wc = WebsiteConfiguration {
xmlns: (), xmlns: (),
error_document: website.error_document.as_ref().map(|v| Key { 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, mut bucket_params,
.. ..
} = ctx; } = ctx;
bucket_params.website_config.update(None); bucket_params.website_config.update(None.into());
garage garage
.bucket_table .bucket_table
.insert(&Bucket::present(bucket_id, bucket_params)) .insert(&Bucket::present(bucket_id, bucket_params))
@@ -83,7 +83,7 @@ pub async fn handle_put_website(
bucket_params bucket_params
.website_config .website_config
.update(Some(conf.into_garage_website_config()?)); .update(Some(conf.into_garage_website_config()?).into());
garage garage
.bucket_table .bucket_table
.insert(&Bucket::present(bucket_id, bucket_params)) .insert(&Bucket::present(bucket_id, bucket_params))
+11 -8
View File
@@ -44,10 +44,13 @@ pub struct LocationConstraint {
pub struct Deleted { pub struct Deleted {
#[serde(rename = "Key")] #[serde(rename = "Key")]
pub key: Value, pub key: Value,
#[serde(rename = "VersionId")] #[serde(rename = "VersionId", skip_serializing_if = "Option::is_none")]
pub version_id: Value, pub version_id: Option<Value>,
#[serde(rename = "DeleteMarkerVersionId")] #[serde(
pub delete_marker_version_id: Value, rename = "DeleteMarkerVersionId",
skip_serializing_if = "Option::is_none"
)]
pub delete_marker_version_id: Option<Value>,
} }
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq, Eq)]
@@ -497,13 +500,13 @@ mod tests {
deleted: vec![ deleted: vec![
Deleted { Deleted {
key: Value("a/plop".to_string()), key: Value("a/plop".to_string()),
version_id: Value("qsdfjklm".to_string()), version_id: Some(Value("qsdfjklm".to_string())),
delete_marker_version_id: Value("wxcvbn".to_string()), delete_marker_version_id: Some(Value("wxcvbn".to_string())),
}, },
Deleted { Deleted {
key: Value("b/plip".to_string()), key: Value("b/plip".to_string()),
version_id: Value("1234".to_string()), version_id: Some(Value("1234".to_string())),
delete_marker_version_id: Value("4321".to_string()), delete_marker_version_id: Some(Value("4321".to_string())),
}, },
], ],
errors: vec![ errors: vec![
+12 -7
View File
@@ -144,7 +144,7 @@ impl BlockManager {
// Open metadata tables // Open metadata tables
let rc = db let rc = db
.open_tree("block_local_rc") .open_typed_tree("block_local_rc")
.expect("Unable to open block_local_rc tree"); .expect("Unable to open block_local_rc tree");
let rc = BlockRc::new(rc); let rc = BlockRc::new(rc);
@@ -158,9 +158,9 @@ impl BlockManager {
let metrics = BlockManagerMetrics::new( let metrics = BlockManagerMetrics::new(
config.compression_level, config.compression_level,
rc.rc_table.clone(), rc.rc_table.untyped().clone(),
resync.queue.clone(), resync.queue.untyped().clone(),
resync.errors.clone(), resync.errors.untyped().clone(),
buffer_kb_semaphore.clone(), buffer_kb_semaphore.clone(),
); );
@@ -224,7 +224,13 @@ impl BlockManager {
|p, tranquility| p.set_with(|x| x.tranquility = tranquility), |p, tranquility| p.set_with(|x| x.tranquility = tranquility),
); );
vars.register_ro(&self.scrub_persister, "scrub-last-completed", |p| { vars.register_ro(&self.scrub_persister, "scrub-last-completed", |p| {
p.get_with(|x| msec_to_rfc3339(x.time_last_complete_scrub)) p.get_with(|x| {
if x.time_last_complete_scrub == 0 {
"never".to_string()
} else {
msec_to_rfc3339(x.time_last_complete_scrub)
}
})
}); });
vars.register_ro(&self.scrub_persister, "scrub-next-run", |p| { vars.register_ro(&self.scrub_persister, "scrub-next-run", |p| {
p.get_with(|x| msec_to_rfc3339(x.time_next_run_scrub)) p.get_with(|x| msec_to_rfc3339(x.time_next_run_scrub))
@@ -443,9 +449,8 @@ impl BlockManager {
let mut blocks = Vec::with_capacity(self.resync.errors.approximate_len()?); let mut blocks = Vec::with_capacity(self.resync.errors.approximate_len()?);
for ent in self.resync.errors.iter()? { for ent in self.resync.errors.iter()? {
let (hash, cnt) = ent?; let (hash, cnt) = ent?;
let cnt = ErrorCounter::decode(&cnt);
blocks.push(BlockResyncErrorInfo { blocks.push(BlockResyncErrorInfo {
hash: Hash::try_from(&hash).unwrap(), hash,
refcount: 0, refcount: 0,
error_count: cnt.errors, error_count: cnt.errors,
last_try: cnt.last_try, last_try: cnt.last_try,
+103 -98
View File
@@ -1,4 +1,5 @@
use std::convert::TryInto; use std::convert::TryInto;
use std::num::NonZeroU64;
use arc_swap::ArcSwapOption; use arc_swap::ArcSwapOption;
@@ -14,12 +15,12 @@ pub type CalculateRefcount =
Box<dyn Fn(&db::Transaction, &Hash) -> db::TxResult<usize, Error> + Send + Sync>; Box<dyn Fn(&db::Transaction, &Hash) -> db::TxResult<usize, Error> + Send + Sync>;
pub struct BlockRc { pub struct BlockRc {
pub rc_table: db::Tree, pub(crate) rc_table: db::TypedTree<Hash, RcEntry>,
pub(crate) recalc_rc: ArcSwapOption<Vec<CalculateRefcount>>, pub(crate) recalc_rc: ArcSwapOption<Vec<CalculateRefcount>>,
} }
impl BlockRc { impl BlockRc {
pub(crate) fn new(rc: db::Tree) -> Self { pub(crate) fn new(rc: db::TypedTree<Hash, RcEntry>) -> Self {
Self { Self {
rc_table: rc, rc_table: rc,
recalc_rc: ArcSwapOption::new(None), recalc_rc: ArcSwapOption::new(None),
@@ -33,11 +34,8 @@ impl BlockRc {
tx: &mut db::Transaction, tx: &mut db::Transaction,
hash: &Hash, hash: &Hash,
) -> db::TxOpResult<bool> { ) -> db::TxOpResult<bool> {
let old_rc = RcEntry::parse_opt(tx.get(&self.rc_table, hash)?); let old_rc = RcState(self.rc_table.tx_get(tx, hash)?);
match old_rc.increment().serialize() { self.rc_table.tx_insert(tx, hash, &old_rc.increment())?;
Some(x) => tx.insert(&self.rc_table, hash, x)?,
None => unreachable!(),
}
Ok(old_rc.is_zero()) Ok(old_rc.is_zero())
} }
@@ -48,17 +46,27 @@ impl BlockRc {
tx: &mut db::Transaction, tx: &mut db::Transaction,
hash: &Hash, hash: &Hash,
) -> db::TxOpResult<bool> { ) -> db::TxOpResult<bool> {
let new_rc = RcEntry::parse_opt(tx.get(&self.rc_table, hash)?).decrement(); let new_rc = RcState(self.rc_table.tx_get(tx, hash)?).decrement();
match new_rc.serialize() { match &new_rc.0 {
Some(x) => tx.insert(&self.rc_table, hash, x)?, None => self.rc_table.tx_remove(tx, hash)?,
None => tx.remove(&self.rc_table, 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 /// Read a block's reference counting state
pub(crate) fn get_block_rc(&self, hash: &Hash) -> Result<RcEntry, Error> { pub(crate) fn get_block_rc(&self, hash: &Hash) -> Result<RcState, Error> {
Ok(RcEntry::parse_opt(self.rc_table.get(hash.as_ref())?)) 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 /// 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> { pub(crate) fn clear_deleted_block_rc(&self, hash: &Hash) -> Result<(), Error> {
let now = now_msec(); let now = now_msec();
self.rc_table.db().transaction(|tx| { self.rc_table.db().transaction(|tx| {
let rcval = RcEntry::parse_opt(tx.get(&self.rc_table, hash)?); let rcval = self.rc_table.tx_get(tx, hash)?;
match rcval { if let Some(RcEntry::Deletable { at_time }) = rcval {
RcEntry::Deletable { at_time } if now > at_time => { if now > at_time {
tx.remove(&self.rc_table, hash)?; self.rc_table.tx_remove(tx, hash)?;
} }
_ => (),
} }
Ok(()) Ok(())
})?; })?;
@@ -91,28 +98,25 @@ impl BlockRc {
for f in recalc_fns.iter() { for f in recalc_fns.iter() {
cnt += f(tx, hash)?; 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!( trace!(
"Block RC for {:?}: stored={}, calculated={}", "Block RC for {:?}: stored={}, calculated={}",
hash, hash,
old_rc.as_u64(), old_count,
cnt cnt
); );
if cnt as u64 != old_rc.as_u64() { if cnt as u64 != old_count {
warn!( warn!(
"Fixing inconsistent block RC for {:?}: was {}, should be {}", "Fixing inconsistent block RC for {:?}: was {}, should be {}",
hash, hash, old_count, cnt
old_rc.as_u64(),
cnt
); );
let new_rc = if cnt > 0 { let new_rc = match NonZeroU64::new(cnt as u64) {
RcEntry::Present { count: cnt as u64 } Some(count) => RcEntry::Present { count },
} else { None => RcEntry::Deletable {
RcEntry::Deletable {
at_time: now_msec() + BLOCK_GC_DELAY.as_millis() as u64, 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)) Ok((cnt, true))
} else { } else {
Ok((cnt, false)) 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)] #[derive(Clone, Copy, Debug)]
pub(crate) enum RcEntry { 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)` /// 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 /// Deletable: the block has zero references, and can be deleted
/// once time (returned by `now_msec`) is larger than `at_time` /// 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 /// (this allows for the data format to be backwards compatible with
/// previous Garage versions that didn't have this intermediate state) /// previous Garage versions that didn't have this intermediate state)
Deletable { at_time: u64 }, Deletable { at_time: u64 },
/// Absent: the block has zero references, and can be deleted
/// immediately
Absent,
} }
impl RcEntry { /// Describes the state of the reference counter for a block: the block's
fn parse(bytes: &[u8]) -> Self { /// entry in the RC table, or `None` if it has none, meaning the block has
if bytes.len() == 8 { /// zero references and can be deleted immediately.
RcEntry::Present { #[derive(Clone, Copy, Debug)]
count: u64::from_be_bytes(bytes.try_into().unwrap()), pub(crate) struct RcState(Option<RcEntry>);
}
} 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
)
}
}
fn parse_opt<V: AsRef<[u8]>>(bytes: Option<V>) -> Self { impl RcState {
bytes /// The new RC table entry after a reference is taken on the block
.map(|b| Self::parse(b.as_ref())) fn increment(&self) -> RcEntry {
.unwrap_or(Self::Absent) let count = match self.0 {
} Some(RcEntry::Present { count }) => count.saturating_add(1),
_ => NonZeroU64::new(1).unwrap(),
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,
}; };
RcEntry::Present { RcEntry::Present { count }
count: old_count + 1,
}
} }
fn decrement(self) -> Self { /// The new state after a reference to the block is dropped
match self { fn decrement(&self) -> Self {
RcEntry::Present { count } => { RcState(match self.0 {
if count > 1 { Some(RcEntry::Present { count }) => Some(match NonZeroU64::new(count.get() - 1) {
RcEntry::Present { count: count - 1 } Some(count) => RcEntry::Present { count },
} else { None => RcEntry::Deletable {
RcEntry::Deletable { at_time: now_msec() + BLOCK_GC_DELAY.as_millis() as u64,
at_time: now_msec() + BLOCK_GC_DELAY.as_millis() as u64, },
} }),
} unchanged => unchanged,
} })
del => del,
}
} }
pub(crate) fn is_zero(&self) -> bool { 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 { pub(crate) fn is_nonzero(&self) -> bool {
@@ -220,10 +225,10 @@ impl RcEntry {
} }
pub(crate) fn is_deletable(&self) -> bool { pub(crate) fn is_deletable(&self) -> bool {
match self { match self.0 {
RcEntry::Present { .. } => false, Some(RcEntry::Present { .. }) => false,
RcEntry::Deletable { at_time } => now_msec() > *at_time, Some(RcEntry::Deletable { at_time }) => now_msec() > at_time,
RcEntry::Absent => true, None => true,
} }
} }
@@ -232,8 +237,8 @@ impl RcEntry {
} }
pub(crate) fn as_u64(&self) -> u64 { pub(crate) fn as_u64(&self) -> u64 {
match self { match self.0 {
RcEntry::Present { count } => *count, Some(RcEntry::Present { count }) => count.get(),
_ => 0, _ => 0,
} }
} }
+3 -4
View File
@@ -102,16 +102,15 @@ impl Worker for RepairWorker {
let mut batch_of_hashes = vec![]; let mut batch_of_hashes = vec![];
let start_bound = match self.next_start.as_ref() { let start_bound = match self.next_start.as_ref() {
None => Bound::Unbounded, None => Bound::Unbounded,
Some(x) => Bound::Excluded(x.as_slice()), Some(x) => Bound::Excluded(*x),
}; };
for entry in self for entry in self
.manager .manager
.rc .rc
.rc_table .rc_table
.range::<&[u8], _>((start_bound, Bound::Unbounded))? .range((start_bound, Bound::Unbounded))?
{ {
let (hash, _) = entry?; let (hash, _) = entry?;
let hash = Hash::try_from(&hash[..]).unwrap();
batch_of_hashes.push(hash); batch_of_hashes.push(hash);
if batch_of_hashes.len() >= 1000 { if batch_of_hashes.len() >= 1000 {
break; break;
@@ -648,7 +647,7 @@ impl BlockStoreIterator {
let mut cum_cap = 0; let mut cum_cap = 0;
let mut todo = vec![]; let mut todo = vec![];
for (dir, cap) in data_layout.data_dirs.iter().zip(dir_cap.into_iter()) { for (dir, cap) in data_layout.data_dirs.iter().zip(dir_cap) {
let progress_min = (cum_cap * PROGRESS_FP) / sum_cap; let progress_min = (cum_cap * PROGRESS_FP) / sum_cap;
let progress_max = ((cum_cap + cap as u64) * PROGRESS_FP) / sum_cap; let progress_max = ((cum_cap + cap as u64) * PROGRESS_FP) / sum_cap;
cum_cap += cap as u64; cum_cap += cap as u64;
+106 -45
View File
@@ -44,15 +44,58 @@ pub(crate) const MAX_RESYNC_WORKERS: usize = 8;
const INITIAL_RESYNC_TRANQUILITY: u32 = 2; const INITIAL_RESYNC_TRANQUILITY: u32 = 2;
pub struct BlockResyncManager { pub struct BlockResyncManager {
pub(crate) queue: db::Tree, pub(crate) queue: db::TypedTree<ResyncQueueKey, Hash>,
pub(crate) notify: Arc<Notify>, pub(crate) notify: Arc<Notify>,
pub(crate) errors: db::Tree, pub(crate) errors: db::TypedTree<Hash, ErrorCounter>,
busy_set: BusySet, busy_set: BusySet,
persister: PersisterShared<ResyncPersistedConfig>, 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)] #[derive(Serialize, Deserialize, Clone, Copy)]
struct ResyncPersistedConfig { struct ResyncPersistedConfig {
n_workers: usize, n_workers: usize,
@@ -74,22 +117,21 @@ enum ResyncIterResult {
IdleFor(Duration), IdleFor(Duration),
} }
type BusySet = Arc<Mutex<HashSet<Vec<u8>>>>; type BusySet = Arc<Mutex<HashSet<ResyncQueueKey>>>;
struct BusyBlock { struct BusyBlock {
time_bytes: Vec<u8>, key: ResyncQueueKey,
hash_bytes: Vec<u8>,
busy_set: BusySet, busy_set: BusySet,
} }
impl BlockResyncManager { impl BlockResyncManager {
pub(crate) fn new(db: &db::Db, system: &System) -> Self { pub(crate) fn new(db: &db::Db, system: &System) -> Self {
let queue = db 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"); .expect("Unable to open block_local_resync_queue tree");
let errors = db 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"); .expect("Unable to open block_local_resync_errors tree");
let persister = PersisterShared::new(&system.metadata_dir, "resync_cfg"); 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 /// Clear the error counter for a block and put it in queue immediately
pub fn clear_backoff(&self, hash: &Hash) -> Result<(), Error> { pub fn clear_backoff(&self, hash: &Hash) -> Result<(), Error> {
let now = now_msec(); let now = now_msec();
if let Some(ec) = self.errors.get(hash)? { if let Some(mut ec) = self.errors.get(hash)? {
let mut ec = ErrorCounter::decode(&ec);
if ec.errors > 0 { if ec.errors > 0 {
ec.last_try = now - ec.delay_msec(); 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)?; self.put_to_resync_at(hash, now)?;
return Ok(()); return Ok(());
} }
@@ -251,23 +292,21 @@ impl BlockResyncManager {
pub(crate) fn put_to_resync_at(&self, hash: &Hash, when: u64) -> db::Result<()> { pub(crate) fn put_to_resync_at(&self, hash: &Hash, when: u64) -> db::Result<()> {
trace!("Put resync_queue: {} {:?}", when, hash); trace!("Put resync_queue: {} {:?}", when, hash);
let mut key = u64::to_be_bytes(when).to_vec(); let qkey = ResyncQueueKey { when, hash: *hash };
key.extend(hash.as_ref()); self.queue.insert(&qkey, hash)?;
self.queue.insert(key, hash.as_ref())?;
self.notify.notify_waiters(); self.notify.notify_waiters();
Ok(()) Ok(())
} }
async fn resync_iter(&self, manager: &BlockManager) -> Result<ResyncIterResult, db::Error> { async fn resync_iter(&self, manager: &BlockManager) -> Result<ResyncIterResult, db::Error> {
if let Some(block) = self.get_block_to_resync()? { 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(); let now = now_msec();
if now >= time_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())? { if let Some(ec) = self.errors.get(&hash)? {
let ec = ErrorCounter::decode(&ec);
if now < ec.next_try() { if now < ec.next_try() {
// if next retry after an error is not yet, // if next retry after an error is not yet,
// don't do resync and return early, but still // don't do resync and return early, but still
@@ -277,7 +316,7 @@ impl BlockResyncManager {
// is not removing the one we added just above // is not removing the one we added just above
// (we want to do the remove after the insert to ensure // (we want to do the remove after the insert to ensure
// that the item is not lost if we crash in-between) // 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); return Ok(ResyncIterResult::BusyDidNothing);
} }
} }
@@ -307,21 +346,21 @@ impl BlockResyncManager {
manager.metrics.resync_error_counter.add(1); manager.metrics.resync_error_counter.add(1);
error!("Error when resyncing {:?}: {}", hash, e); error!("Error when resyncing {:?}: {}", hash, e);
let err_counter = match self.errors.get(hash.as_slice())? { let err_counter = match self.errors.get(&hash)? {
Some(ec) => ErrorCounter::decode(&ec).add1(now + 1), Some(ec) => ec.add1(now + 1),
None => ErrorCounter::new(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())?; self.put_to_resync_at(&hash, err_counter.next_try())?;
// err_counter.next_try() >= now + 1 > now, // err_counter.next_try() >= now + 1 > now,
// the entry we remove from the queue is not // the entry we remove from the queue is not
// the entry we inserted with put_to_resync_at // the entry we inserted with put_to_resync_at
self.queue.remove(&block.time_bytes)?; self.queue.remove(&block.key)?;
} else { } else {
self.errors.remove(hash.as_slice())?; self.errors.remove(&hash)?;
self.queue.remove(&block.time_bytes)?; self.queue.remove(&block.key)?;
} }
Ok(ResyncIterResult::BusyDidSomething) Ok(ResyncIterResult::BusyDidSomething)
@@ -344,12 +383,11 @@ impl BlockResyncManager {
fn get_block_to_resync(&self) -> Result<Option<BusyBlock>, db::Error> { fn get_block_to_resync(&self) -> Result<Option<BusyBlock>, db::Error> {
let mut busy = self.busy_set.lock().unwrap(); let mut busy = self.busy_set.lock().unwrap();
for it in self.queue.iter()? { for it in self.queue.iter()? {
let (time_bytes, hash_bytes) = it?; let (key, _) = it?;
if !busy.contains(&time_bytes) { if !busy.contains(&key) {
busy.insert(time_bytes.clone()); busy.insert(key);
return Ok(Some(BusyBlock { return Ok(Some(BusyBlock {
time_bytes, key,
hash_bytes,
busy_set: self.busy_set.clone(), busy_set: self.busy_set.clone(),
})); }));
} }
@@ -506,7 +544,7 @@ impl BlockResyncManager {
impl Drop for BusyBlock { impl Drop for BusyBlock {
fn drop(&mut self) { fn drop(&mut self) {
let mut busy = self.busy_set.lock().unwrap(); 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, tranquilizer: Tranquilizer,
next_delay: Duration, next_delay: Duration,
persister: PersisterShared<ResyncPersistedConfig>, persister: PersisterShared<ResyncPersistedConfig>,
had_decode_error: bool,
} }
impl ResyncWorker { impl ResyncWorker {
@@ -527,6 +566,7 @@ impl ResyncWorker {
tranquilizer: Tranquilizer::new(30), tranquilizer: Tranquilizer::new(30),
next_delay: Duration::from_secs(10), next_delay: Duration::from_secs(10),
persister, persister,
had_decode_error: false,
} }
} }
} }
@@ -574,6 +614,15 @@ impl Worker for ResyncWorker {
self.next_delay = delay; self.next_delay = delay;
Ok(WorkerState::Idle) 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) => { Err(e) => {
// The errors that we have here are only db errors // The errors that we have here are only db errors
// We don't really know how to handle them so just ¯\_(ツ)_/¯ // 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?) // 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, // Here we just give the error to the worker manager,
// it will print it to the logs and increment a counter // it will print it to the logs and increment a counter
self.had_decode_error = false;
Err(e.into()) Err(e.into())
} }
} }
@@ -602,6 +652,7 @@ impl Worker for ResyncWorker {
/// Counts the number of errors when resyncing a block, /// Counts the number of errors when resyncing a block,
/// and the time of the last try. /// and the time of the last try.
///
/// Used to implement exponential backoff. /// Used to implement exponential backoff.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub(crate) struct ErrorCounter { pub(crate) struct ErrorCounter {
@@ -609,6 +660,31 @@ pub(crate) struct ErrorCounter {
pub(crate) last_try: u64, 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 { impl ErrorCounter {
fn new(now: u64) -> Self { fn new(now: u64) -> Self {
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 { fn add1(self, now: u64) -> Self {
Self { Self {
errors: self.errors + 1, errors: self.errors + 1,
+56 -50
View File
@@ -12,7 +12,7 @@ use fjall::{
use crate::{ use crate::{
open::{Engine, OpenOpt}, 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, 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()); info!("Opening Fjall database at: {}", path.display());
if opt.fsync { if opt.fsync {
return Err(Error( return Err(DbError(
"metadata_fsync is not supported with the Fjall database engine".into(), "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 // -- err
impl From<fjall::Error> for Error { impl From<fjall::Error> for DbError {
fn from(e: fjall::Error) -> Error { fn from(e: fjall::Error) -> DbError {
Error(format!("fjall: {}", e).into()) DbError(format!("fjall: {}", e).into())
} }
} }
impl From<fjall::LsmError> for Error { impl From<fjall::LsmError> for DbError {
fn from(e: fjall::LsmError) -> Error { fn from(e: fjall::LsmError) -> DbError {
Error(format!("fjall lsm_tree: {}", e).into()) 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 { impl From<fjall::Error> for TxOpError {
fn from(e: fjall::Error) -> TxOpError { fn from(e: fjall::Error) -> TxOpError {
TxOpError(e.into()) DbError::from(e).into()
} }
} }
@@ -76,11 +82,11 @@ impl FjallDb {
fn get_tree( fn get_tree(
&self, &self,
i: usize, i: usize,
) -> Result<MappedRwLockReadGuard<'_, TransactionalPartitionHandle>> { ) -> DbResult<MappedRwLockReadGuard<'_, TransactionalPartitionHandle>> {
RwLockReadGuard::try_map(self.trees.read(), |trees: &Vec<_>| { RwLockReadGuard::try_map(self.trees.read(), |trees: &Vec<_>| {
trees.get(i).map(|tup| &tup.1) 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() "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 mut trees = self.trees.write();
let safe_name = encode_name(name)?; let safe_name = encode_name(name)?;
if let Some(i) = trees.iter().position(|(name, _)| *name == safe_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 self.keyspace
.list_partitions() .list_partitions()
.iter() .iter()
.map(|n| decode_name(n)) .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)?; std::fs::create_dir_all(base_path)?;
let path = Engine::Fjall.db_path(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 tree = self.get_tree(tree_idx)?;
let tx = self.keyspace.read_tx(); let tx = self.keyspace.read_tx();
let val = tx.get(&tree, key)?; 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)?; let tree = self.get_tree(tree_idx)?;
Ok(tree.approximate_len()) 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 tree = self.get_tree(tree_idx)?;
let tx = self.keyspace.read_tx(); let tx = self.keyspace.read_tx();
Ok(tx.is_empty(&tree)?) 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 tree = self.get_tree(tree_idx)?;
let mut tx = self.keyspace.write_tx(); let mut tx = self.keyspace.write_tx();
tx.insert(&tree, key, value); tx.insert(&tree, key, value);
@@ -166,7 +172,7 @@ impl IDb for FjallDb {
Ok(()) 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 tree = self.get_tree(tree_idx)?;
let mut tx = self.keyspace.write_tx(); let mut tx = self.keyspace.write_tx();
tx.remove(&tree, key); tx.remove(&tree, key);
@@ -174,11 +180,11 @@ impl IDb for FjallDb {
Ok(()) Ok(())
} }
fn clear(&self, tree_idx: usize) -> Result<()> { fn clear(&self, tree_idx: usize) -> DbResult<()> {
let mut trees = self.trees.write(); let mut trees = self.trees.write();
if tree_idx >= trees.len() { 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); let (name, tree) = trees.remove(tree_idx);
@@ -191,13 +197,13 @@ impl IDb for FjallDb {
Ok(()) 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 tree = self.get_tree(tree_idx)?;
let tx = self.keyspace.read_tx(); let tx = self.keyspace.read_tx();
Ok(Box::new(tx.iter(&tree).map(iterator_remap))) 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 tree = self.get_tree(tree_idx)?;
let tx = self.keyspace.read_tx(); let tx = self.keyspace.read_tx();
Ok(Box::new(tx.iter(&tree).rev().map(iterator_remap))) Ok(Box::new(tx.iter(&tree).rev().map(iterator_remap)))
@@ -208,7 +214,7 @@ impl IDb for FjallDb {
tree_idx: usize, tree_idx: usize,
low: Bound<&'r [u8]>, low: Bound<&'r [u8]>,
high: Bound<&'r [u8]>, high: Bound<&'r [u8]>,
) -> Result<ValueIter<'_>> { ) -> DbResult<ValueIter<'_>> {
let tree = self.get_tree(tree_idx)?; let tree = self.get_tree(tree_idx)?;
let tx = self.keyspace.read_tx(); let tx = self.keyspace.read_tx();
Ok(Box::new( Ok(Box::new(
@@ -221,7 +227,7 @@ impl IDb for FjallDb {
tree_idx: usize, tree_idx: usize,
low: Bound<&'r [u8]>, low: Bound<&'r [u8]>,
high: Bound<&'r [u8]>, high: Bound<&'r [u8]>,
) -> Result<ValueIter<'_>> { ) -> DbResult<ValueIter<'_>> {
let tree = self.get_tree(tree_idx)?; let tree = self.get_tree(tree_idx)?;
let tx = self.keyspace.read_tx(); let tx = self.keyspace.read_tx();
Ok(Box::new( Ok(Box::new(
@@ -252,9 +258,9 @@ impl IDb for FjallDb {
} }
TxFnResult::DbErr => { TxFnResult::DbErr => {
tx.tx.rollback(); tx.tx.rollback();
Err(TxError::Db(Error( Err(TxError::Db(
"(this message will be discarded)".into(), DbError("(this message will be discarded)".into()).into(),
))) ))
} }
} }
} }
@@ -268,47 +274,47 @@ struct FjallTx<'a> {
} }
impl<'a> 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(|| { 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(), "invalid tree id (it might have been opened after the transaction started)".into(),
)) )
}) })
} }
} }
impl<'a> ITx for FjallTx<'a> { 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)?; let tree = self.get_tree(tree_idx)?;
match self.tx.get(tree, key)? { match self.tx.get(tree, key)? {
Some(v) => Ok(Some(v.to_vec())), Some(v) => Ok(Some(v.to_vec())),
None => Ok(None), 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)?; let tree = self.get_tree(tree_idx)?;
Ok(self.tx.len(tree)?) 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(); let tree = self.get_tree(tree_idx)?.clone();
self.tx.insert(&tree, key, value); self.tx.insert(&tree, key, value);
Ok(()) 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(); let tree = self.get_tree(tree_idx)?.clone();
self.tx.remove(&tree, key); self.tx.remove(&tree, key);
Ok(()) 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") 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(); let tree = self.get_tree(tree_idx)?.clone();
Ok(Box::new(self.tx.iter(&tree).map(iterator_remap_tx))) 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(); let tree = self.get_tree(tree_idx)?.clone();
Ok(Box::new(self.tx.iter(&tree).rev().map(iterator_remap_tx))) 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, tree_idx: usize,
low: Bound<&'r [u8]>, low: Bound<&'r [u8]>,
high: Bound<&'r [u8]>, high: Bound<&'r [u8]>,
) -> TxOpResult<TxValueIter<'_>> { ) -> DbResult<TxValueIter<'_>> {
let tree = self.get_tree(tree_idx)?; let tree = self.get_tree(tree_idx)?;
let low = clone_bound(low); let low = clone_bound(low);
let high = clone_bound(high); let high = clone_bound(high);
@@ -333,7 +339,7 @@ impl<'a> ITx for FjallTx<'a> {
tree_idx: usize, tree_idx: usize,
low: Bound<&'r [u8]>, low: Bound<&'r [u8]>,
high: Bound<&'r [u8]>, high: Bound<&'r [u8]>,
) -> TxOpResult<TxValueIter<'_>> { ) -> DbResult<TxValueIter<'_>> {
let tree = self.get_tree(tree_idx)?; let tree = self.get_tree(tree_idx)?;
let low = clone_bound(low); let low = clone_bound(low);
let high = clone_bound(high); let high = clone_bound(high);
@@ -348,14 +354,14 @@ impl<'a> ITx for FjallTx<'a> {
// -- maps fjall's (k, v) to ours // -- 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())) 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())) 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 // -- 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 -- // -- 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 base = 'A' as u32;
let mut ret = String::with_capacity(s.len() + 10); 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_hi).unwrap());
ret.push(char::from_u32(base + c_lo).unwrap()); ret.push(char::from_u32(base + c_lo).unwrap());
} else { } else {
return Err(Error( return Err(DbError(
format!("table name {} could not be safely encoded", s).into(), format!("table name {} could not be safely encoded", s).into(),
)); ));
} }
@@ -400,10 +406,10 @@ fn encode_name(s: &str) -> Result<String> {
Ok(ret) Ok(ret)
} }
fn decode_name(s: &str) -> Result<String> { fn decode_name(s: &str) -> DbResult<String> {
use std::convert::TryFrom; 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_map = |c: char| {
let c = c as u32; let c = c as u32;
let base = 'A' as u32; let base = 'A' as u32;
+97 -57
View File
@@ -9,6 +9,7 @@ pub mod lmdb_adapter;
pub mod sqlite_adapter; pub mod sqlite_adapter;
pub mod open; pub mod open;
pub mod typed;
#[cfg(test)] #[cfg(test)]
pub mod test; pub mod test;
@@ -23,6 +24,7 @@ use std::sync::Arc;
use thiserror::Error; use thiserror::Error;
pub use open::*; pub use open::*;
pub use typed::{DbBytes, DbOrdKey, TypedIter, TypedTree, TypedTxIter};
pub(crate) type OnCommit = Vec<Box<dyn FnOnce()>>; pub(crate) type OnCommit = Vec<Box<dyn FnOnce()>>;
@@ -38,21 +40,34 @@ pub struct Transaction<'a> {
pub struct Tree(Arc<dyn IDb>, usize); pub struct Tree(Arc<dyn IDb>, usize);
pub type Value = Vec<u8>; pub type Value = Vec<u8>;
pub type ValueIter<'a> = Box<dyn std::iter::Iterator<Item = Result<(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 = TxOpResult<(Value, Value)>> + 'a>; pub type TxValueIter<'a> = Box<dyn std::iter::Iterator<Item = DbResult<(Value, Value)>> + 'a>;
// ---- // ----
#[derive(Debug, Error)] #[derive(Debug, Error)]
#[error("{0}")] #[error("database error: {0}")]
pub struct Error(pub Cow<'static, str>); pub struct DbError(pub Cow<'static, str>);
impl From<std::io::Error> for Error { #[derive(Debug, Error)]
fn from(e: std::io::Error) -> Error { #[error("decode error: {0}")]
Error(format!("IO: {}", e).into()) 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>; pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -60,6 +75,18 @@ pub type Result<T> = std::result::Result<T, Error>;
pub struct TxOpError(pub(crate) Error); pub struct TxOpError(pub(crate) Error);
pub type TxOpResult<T> = std::result::Result<T, TxOpError>; 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)] #[derive(Debug)]
pub enum TxError<E> { pub enum TxError<E> {
Abort(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>> { pub fn unabort<R, E>(res: TxResult<R, E>) -> TxOpResult<std::result::Result<R, E>> {
match res { match res {
Ok(v) => Ok(Ok(v)), Ok(v) => Ok(Ok(v)),
@@ -88,12 +121,12 @@ impl Db {
self.0.engine() 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())?; let tree_id = self.0.open_tree(name.as_ref())?;
Ok(Tree(self.0.clone(), tree_id)) 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() 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) self.0.snapshot(path)
} }
pub fn import(&self, other: &Db) -> Result<()> { pub fn import(&self, other: &Db) -> Result<()> {
let existing_trees = self.list_trees()?; let existing_trees = self.list_trees()?;
if !existing_trees.is_empty() { if !existing_trees.is_empty() {
return Err(Error( return Err(DbError(
format!( format!(
"destination database already contains data: {:?}", "destination database already contains data: {:?}",
existing_trees existing_trees
) )
.into(), .into(),
)); )
.into());
} }
let tree_names = other.list_trees()?; let tree_names = other.list_trees()?;
for name in tree_names { for name in tree_names {
let tree = self.open_tree(&name)?; let tree = self.open_tree(&name)?;
if !tree.is_empty()? { 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)?; let ex_tree = other.open_tree(&name)?;
@@ -186,7 +220,7 @@ impl Db {
}); });
let total = match tx_res { let total = match tx_res {
Err(TxError::Db(e)) => return Err(e), 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, Ok(x) => x,
}; };
@@ -204,24 +238,24 @@ impl Tree {
} }
#[inline] #[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()) self.0.get(self.1, key.as_ref())
} }
#[inline] #[inline]
pub fn approximate_len(&self) -> Result<usize> { pub fn approximate_len(&self) -> DbResult<usize> {
self.0.approximate_len(self.1) self.0.approximate_len(self.1)
} }
#[inline] #[inline]
pub fn is_empty(&self) -> Result<bool> { pub fn is_empty(&self) -> DbResult<bool> {
self.0.is_empty(self.1) self.0.is_empty(self.1)
} }
#[inline] #[inline]
pub fn first(&self) -> Result<Option<(Value, Value)>> { pub fn first(&self) -> DbResult<Option<(Value, Value)>> {
self.iter()?.next().transpose() self.iter()?.next().transpose()
} }
#[inline] #[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() { if from.as_ref().is_empty() {
self.iter()?.next().transpose() self.iter()?.next().transpose()
} else { } else {
@@ -233,31 +267,31 @@ impl Tree {
/// Returns the old value if there was one /// Returns the old value if there was one
#[inline] #[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()) self.0.insert(self.1, key.as_ref(), value.as_ref())
} }
/// Returns the old value if there was one /// Returns the old value if there was one
#[inline] #[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()) self.0.remove(self.1, key.as_ref())
} }
/// Clears all values from the tree /// Clears all values from the tree
#[inline] #[inline]
pub fn clear(&self) -> Result<()> { pub fn clear(&self) -> DbResult<()> {
self.0.clear(self.1) self.0.clear(self.1)
} }
#[inline] #[inline]
pub fn iter(&self) -> Result<ValueIter<'_>> { pub fn iter(&self) -> DbResult<ValueIter<'_>> {
self.0.iter(self.1) self.0.iter(self.1)
} }
#[inline] #[inline]
pub fn iter_rev(&self) -> Result<ValueIter<'_>> { pub fn iter_rev(&self) -> DbResult<ValueIter<'_>> {
self.0.iter_rev(self.1) self.0.iter_rev(self.1)
} }
#[inline] #[inline]
pub fn range<K, R>(&self, range: R) -> Result<ValueIter<'_>> pub fn range<K, R>(&self, range: R) -> DbResult<ValueIter<'_>>
where where
K: AsRef<[u8]>, K: AsRef<[u8]>,
R: RangeBounds<K>, R: RangeBounds<K>,
@@ -267,7 +301,7 @@ impl Tree {
self.0.range(self.1, get_bound(sb), get_bound(eb)) self.0.range(self.1, get_bound(sb), get_bound(eb))
} }
#[inline] #[inline]
pub fn range_rev<K, R>(&self, range: R) -> Result<ValueIter<'_>> pub fn range_rev<K, R>(&self, range: R) -> DbResult<ValueIter<'_>>
where where
K: AsRef<[u8]>, K: AsRef<[u8]>,
R: RangeBounds<K>, R: RangeBounds<K>,
@@ -282,11 +316,11 @@ impl Tree {
impl<'a> Transaction<'a> { impl<'a> Transaction<'a> {
#[inline] #[inline]
pub fn get<T: AsRef<[u8]>>(&self, tree: &Tree, key: T) -> TxOpResult<Option<Value>> { 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] #[inline]
pub fn len(&self, tree: &Tree) -> TxOpResult<usize> { 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 /// Returns the old value if there was one
@@ -297,26 +331,28 @@ impl<'a> Transaction<'a> {
key: T, key: T,
value: U, value: U,
) -> TxOpResult<()> { ) -> 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 /// Returns the old value if there was one
#[inline] #[inline]
pub fn remove<T: AsRef<[u8]>>(&mut self, tree: &Tree, key: T) -> TxOpResult<()> { 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 /// Clears all values in a tree
#[inline] #[inline]
pub fn clear(&mut self, tree: &Tree) -> TxOpResult<()> { pub fn clear(&mut self, tree: &Tree) -> TxOpResult<()> {
self.tx.clear(tree.1) self.tx.clear(tree.1).map_err(Into::into)
} }
#[inline] #[inline]
pub fn iter(&self, tree: &Tree) -> TxOpResult<TxValueIter<'_>> { pub fn iter(&self, tree: &Tree) -> TxOpResult<TxValueIter<'_>> {
self.tx.iter(tree.1) self.tx.iter(tree.1).map_err(Into::into)
} }
#[inline] #[inline]
pub fn iter_rev(&self, tree: &Tree) -> TxOpResult<TxValueIter<'_>> { 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] #[inline]
@@ -327,7 +363,9 @@ impl<'a> Transaction<'a> {
{ {
let sb = range.start_bound(); let sb = range.start_bound();
let eb = range.end_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] #[inline]
pub fn range_rev<K, R>(&self, tree: &Tree, range: R) -> TxOpResult<TxValueIter<'_>> 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 sb = range.start_bound();
let eb = range.end_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] #[inline]
@@ -350,60 +390,60 @@ impl<'a> Transaction<'a> {
pub(crate) trait IDb: Send + Sync { pub(crate) trait IDb: Send + Sync {
fn engine(&self) -> String; fn engine(&self) -> String;
fn open_tree(&self, name: &str) -> Result<usize>; fn open_tree(&self, name: &str) -> DbResult<usize>;
fn list_trees(&self) -> Result<Vec<String>>; fn list_trees(&self) -> DbResult<Vec<String>>;
fn snapshot(&self, path: &Path) -> Result<()>; fn snapshot(&self, path: &Path) -> DbResult<()>;
fn get(&self, tree: usize, key: &[u8]) -> Result<Option<Value>>; fn get(&self, tree: usize, key: &[u8]) -> DbResult<Option<Value>>;
fn approximate_len(&self, tree: usize) -> Result<usize>; fn approximate_len(&self, tree: usize) -> DbResult<usize>;
fn is_empty(&self, tree: usize) -> Result<bool>; fn is_empty(&self, tree: usize) -> DbResult<bool>;
fn insert(&self, tree: usize, key: &[u8], value: &[u8]) -> Result<()>; fn insert(&self, tree: usize, key: &[u8], value: &[u8]) -> DbResult<()>;
fn remove(&self, tree: usize, key: &[u8]) -> Result<()>; fn remove(&self, tree: usize, key: &[u8]) -> DbResult<()>;
fn clear(&self, tree: usize) -> Result<()>; fn clear(&self, tree: usize) -> DbResult<()>;
fn iter(&self, tree: usize) -> Result<ValueIter<'_>>; fn iter(&self, tree: usize) -> DbResult<ValueIter<'_>>;
fn iter_rev(&self, tree: usize) -> Result<ValueIter<'_>>; fn iter_rev(&self, tree: usize) -> DbResult<ValueIter<'_>>;
fn range<'r>( fn range<'r>(
&self, &self,
tree: usize, tree: usize,
low: Bound<&'r [u8]>, low: Bound<&'r [u8]>,
high: Bound<&'r [u8]>, high: Bound<&'r [u8]>,
) -> Result<ValueIter<'_>>; ) -> DbResult<ValueIter<'_>>;
fn range_rev<'r>( fn range_rev<'r>(
&self, &self,
tree: usize, tree: usize,
low: Bound<&'r [u8]>, low: Bound<&'r [u8]>,
high: Bound<&'r [u8]>, high: Bound<&'r [u8]>,
) -> Result<ValueIter<'_>>; ) -> DbResult<ValueIter<'_>>;
fn transaction(&self, f: &dyn ITxFn) -> TxResult<OnCommit, ()>; fn transaction(&self, f: &dyn ITxFn) -> TxResult<OnCommit, ()>;
} }
pub(crate) trait ITx { pub(crate) trait ITx {
fn get(&self, tree: usize, key: &[u8]) -> TxOpResult<Option<Value>>; fn get(&self, tree: usize, key: &[u8]) -> DbResult<Option<Value>>;
fn len(&self, tree: usize) -> TxOpResult<usize>; fn len(&self, tree: usize) -> DbResult<usize>;
fn insert(&mut self, tree: usize, key: &[u8], value: &[u8]) -> TxOpResult<()>; fn insert(&mut self, tree: usize, key: &[u8], value: &[u8]) -> DbResult<()>;
fn remove(&mut self, tree: usize, key: &[u8]) -> TxOpResult<()>; fn remove(&mut self, tree: usize, key: &[u8]) -> DbResult<()>;
fn clear(&mut self, tree: usize) -> TxOpResult<()>; fn clear(&mut self, tree: usize) -> DbResult<()>;
fn iter(&self, tree: usize) -> TxOpResult<TxValueIter<'_>>; fn iter(&self, tree: usize) -> DbResult<TxValueIter<'_>>;
fn iter_rev(&self, tree: usize) -> TxOpResult<TxValueIter<'_>>; fn iter_rev(&self, tree: usize) -> DbResult<TxValueIter<'_>>;
fn range<'r>( fn range<'r>(
&self, &self,
tree: usize, tree: usize,
low: Bound<&'r [u8]>, low: Bound<&'r [u8]>,
high: Bound<&'r [u8]>, high: Bound<&'r [u8]>,
) -> TxOpResult<TxValueIter<'_>>; ) -> DbResult<TxValueIter<'_>>;
fn range_rev<'r>( fn range_rev<'r>(
&self, &self,
tree: usize, tree: usize,
low: Bound<&'r [u8]>, low: Bound<&'r [u8]>,
high: Bound<&'r [u8]>, high: Bound<&'r [u8]>,
) -> TxOpResult<TxValueIter<'_>>; ) -> DbResult<TxValueIter<'_>>;
} }
pub(crate) trait ITxFn { pub(crate) trait ITxFn {
+49 -43
View File
@@ -14,7 +14,7 @@ type Database = heed::Database<Bytes, Bytes>;
use crate::{ use crate::{
open::{Engine, OpenOpt}, 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, TxResult, TxValueIter, Value, ValueIter,
}; };
@@ -22,10 +22,10 @@ pub use heed;
// ---- top-level open function // ---- 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()); info!("Opening LMDB database at: {}", path.display());
if let Err(e) = std::fs::create_dir_all(path) { 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(), 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) env_builder.open(path)
}; };
match open_res { 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 \ "OutOfMemory error while trying to open LMDB database. This can happen \
if your operating system is not allowing you to use sufficient virtual \ if your operating system is not allowing you to use sufficient virtual \
memory address space. Please check that no limit is set (ulimit -v). \ 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." On 32-bit machines, you should probably switch to another database engine."
.into(), .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)), Ok(db) => Ok(LmdbDb::init(db)),
} }
} }
// -- err // -- err
impl From<heed::Error> for DbError {
fn from(e: heed::Error) -> DbError {
DbError(format!("LMDB: {}", e).into())
}
}
impl From<heed::Error> for Error { impl From<heed::Error> for Error {
fn from(e: heed::Error) -> Error { fn from(e: heed::Error) -> Error {
Error(format!("LMDB: {}", e).into()) DbError::from(e).into()
} }
} }
impl From<heed::Error> for TxOpError { impl From<heed::Error> for TxOpError {
fn from(e: heed::Error) -> TxOpError { fn from(e: heed::Error) -> TxOpError {
TxOpError(e.into()) DbError::from(e).into()
} }
} }
@@ -91,14 +97,14 @@ impl LmdbDb {
Db(Arc::new(s)) Db(Arc::new(s))
} }
fn get_tree(&self, i: usize) -> Result<Database> { fn get_tree(&self, i: usize) -> DbResult<Database> {
self.trees self.trees
.read() .read()
.unwrap() .unwrap()
.0 .0
.get(i) .get(i)
.cloned() .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() "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(); let mut trees = self.trees.write().unwrap();
if let Some(i) = trees.1.get(name) { if let Some(i) = trees.1.get(name) {
Ok(*i) 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 rtxn = self.db.read_txn()?;
let tree0 = match self let tree0 = match self
.db .db
@@ -153,7 +159,7 @@ impl IDb for LmdbDb {
Ok(ret2) Ok(ret2)
} }
fn snapshot(&self, base_path: &Path) -> Result<()> { fn snapshot(&self, base_path: &Path) -> DbResult<()> {
std::fs::create_dir_all(base_path)?; std::fs::create_dir_all(base_path)?;
let path = Engine::Lmdb.db_path(base_path); let path = Engine::Lmdb.db_path(base_path);
self.db 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 tree = self.get_tree(tree)?;
let tx = self.db.read_txn()?; 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 tree = self.get_tree(tree)?;
let tx = self.db.read_txn()?; let tx = self.db.read_txn()?;
Ok(tree.len(&tx)?.try_into().unwrap()) 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 tree = self.get_tree(tree)?;
let tx = self.db.read_txn()?; let tx = self.db.read_txn()?;
Ok(tree.is_empty(&tx)?) 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 tree = self.get_tree(tree)?;
let mut tx = self.db.write_txn()?; let mut tx = self.db.write_txn()?;
tree.put(&mut tx, key, value)?; tree.put(&mut tx, key, value)?;
@@ -193,7 +199,7 @@ impl IDb for LmdbDb {
Ok(()) Ok(())
} }
fn remove(&self, tree: usize, key: &[u8]) -> Result<()> { fn remove(&self, tree: usize, key: &[u8]) -> DbResult<()> {
let tree = self.get_tree(tree)?; let tree = self.get_tree(tree)?;
let mut tx = self.db.write_txn()?; let mut tx = self.db.write_txn()?;
tree.delete(&mut tx, key)?; tree.delete(&mut tx, key)?;
@@ -201,7 +207,7 @@ impl IDb for LmdbDb {
Ok(()) Ok(())
} }
fn clear(&self, tree: usize) -> Result<()> { fn clear(&self, tree: usize) -> DbResult<()> {
let tree = self.get_tree(tree)?; let tree = self.get_tree(tree)?;
let mut tx = self.db.write_txn()?; let mut tx = self.db.write_txn()?;
tree.clear(&mut tx)?; tree.clear(&mut tx)?;
@@ -209,14 +215,14 @@ impl IDb for LmdbDb {
Ok(()) Ok(())
} }
fn iter(&self, tree: usize) -> Result<ValueIter<'_>> { fn iter(&self, tree: usize) -> DbResult<ValueIter<'_>> {
let tree = self.get_tree(tree)?; let tree = self.get_tree(tree)?;
let tx = self.db.read_txn()?; let tx = self.db.read_txn()?;
// Safety: the cloture does not store its argument anywhere, // Safety: the cloture does not store its argument anywhere,
unsafe { TxAndIterator::make(tx, |tx| Ok(tree.iter(tx)?)) } 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 tree = self.get_tree(tree)?;
let tx = self.db.read_txn()?; let tx = self.db.read_txn()?;
// Safety: the cloture does not store its argument anywhere, // Safety: the cloture does not store its argument anywhere,
@@ -228,7 +234,7 @@ impl IDb for LmdbDb {
tree: usize, tree: usize,
low: Bound<&'r [u8]>, low: Bound<&'r [u8]>,
high: Bound<&'r [u8]>, high: Bound<&'r [u8]>,
) -> Result<ValueIter<'_>> { ) -> DbResult<ValueIter<'_>> {
let tree = self.get_tree(tree)?; let tree = self.get_tree(tree)?;
let tx = self.db.read_txn()?; let tx = self.db.read_txn()?;
// Safety: the cloture does not store its argument anywhere, // Safety: the cloture does not store its argument anywhere,
@@ -239,7 +245,7 @@ impl IDb for LmdbDb {
tree: usize, tree: usize,
low: Bound<&'r [u8]>, low: Bound<&'r [u8]>,
high: Bound<&'r [u8]>, high: Bound<&'r [u8]>,
) -> Result<ValueIter<'_>> { ) -> DbResult<ValueIter<'_>> {
let tree = self.get_tree(tree)?; let tree = self.get_tree(tree)?;
let tx = self.db.read_txn()?; let tx = self.db.read_txn()?;
// Safety: the cloture does not store its argument anywhere, // Safety: the cloture does not store its argument anywhere,
@@ -271,9 +277,9 @@ impl IDb for LmdbDb {
} }
TxFnResult::DbErr => { TxFnResult::DbErr => {
tx.tx.abort(); tx.tx.abort();
Err(TxError::Db(Error( Err(TxError::Db(
"(this message will be discarded)".into(), DbError("(this message will be discarded)".into()).into(),
))) ))
} }
} }
} }
@@ -287,49 +293,49 @@ struct LmdbTx<'a> {
} }
impl<'a> 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(|| { self.trees.get(i).ok_or_else(|| {
TxOpError(Error( DbError(
"invalid tree id (it might have been opened after the transaction started)".into(), "invalid tree id (it might have been opened after the transaction started)".into(),
)) )
}) })
} }
} }
impl<'a> ITx for LmdbTx<'a> { 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)?; let tree = self.get_tree(tree)?;
match tree.get(&self.tx, key)? { match tree.get(&self.tx, key)? {
Some(v) => Ok(Some(v.to_vec())), Some(v) => Ok(Some(v.to_vec())),
None => Ok(None), None => Ok(None),
} }
} }
fn len(&self, tree: usize) -> TxOpResult<usize> { fn len(&self, tree: usize) -> DbResult<usize> {
let tree = self.get_tree(tree)?; let tree = self.get_tree(tree)?;
Ok(tree.len(&self.tx)? as usize) 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)?; let tree = *self.get_tree(tree)?;
tree.put(&mut self.tx, key, value)?; tree.put(&mut self.tx, key, value)?;
Ok(()) 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)?; let tree = *self.get_tree(tree)?;
tree.delete(&mut self.tx, key)?; tree.delete(&mut self.tx, key)?;
Ok(()) Ok(())
} }
fn clear(&mut self, tree: usize) -> TxOpResult<()> { fn clear(&mut self, tree: usize) -> DbResult<()> {
let tree = *self.get_tree(tree)?; let tree = *self.get_tree(tree)?;
tree.clear(&mut self.tx)?; tree.clear(&mut self.tx)?;
Ok(()) Ok(())
} }
fn iter(&self, tree: usize) -> TxOpResult<TxValueIter<'_>> { fn iter(&self, tree: usize) -> DbResult<TxValueIter<'_>> {
let tree = *self.get_tree(tree)?; let tree = *self.get_tree(tree)?;
Ok(Box::new(tree.iter(&self.tx)?.map(tx_iter_item))) 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)?; let tree = *self.get_tree(tree)?;
Ok(Box::new(tree.rev_iter(&self.tx)?.map(tx_iter_item))) Ok(Box::new(tree.rev_iter(&self.tx)?.map(tx_iter_item)))
} }
@@ -339,7 +345,7 @@ impl<'a> ITx for LmdbTx<'a> {
tree: usize, tree: usize,
low: Bound<&'r [u8]>, low: Bound<&'r [u8]>,
high: Bound<&'r [u8]>, high: Bound<&'r [u8]>,
) -> TxOpResult<TxValueIter<'_>> { ) -> DbResult<TxValueIter<'_>> {
let tree = *self.get_tree(tree)?; let tree = *self.get_tree(tree)?;
Ok(Box::new( Ok(Box::new(
tree.range(&self.tx, &(low, high))?.map(tx_iter_item), tree.range(&self.tx, &(low, high))?.map(tx_iter_item),
@@ -350,7 +356,7 @@ impl<'a> ITx for LmdbTx<'a> {
tree: usize, tree: usize,
low: Bound<&'r [u8]>, low: Bound<&'r [u8]>,
high: Bound<&'r [u8]>, high: Bound<&'r [u8]>,
) -> TxOpResult<TxValueIter<'_>> { ) -> DbResult<TxValueIter<'_>> {
let tree = *self.get_tree(tree)?; let tree = *self.get_tree(tree)?;
Ok(Box::new( Ok(Box::new(
tree.rev_range(&self.tx, &(low, high))?.map(tx_iter_item), 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. /// 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 where
F: FnOnce(&'a RoTxn<'a>) -> Result<I>, F: FnOnce(&'a RoTxn<'a>) -> DbResult<I>,
{ {
let res = TxAndIterator { let res = TxAndIterator {
tx, tx,
@@ -436,13 +442,13 @@ impl<'a, I> Iterator for TxAndIteratorPin<'a, I>
where where
I: Iterator<Item = IteratorItem<'a>> + 'a, I: Iterator<Item = IteratorItem<'a>> + 'a,
{ {
type Item = Result<(Value, Value)>; type Item = DbResult<(Value, Value)>;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let mut_ref = Pin::as_mut(&mut self.0); let mut_ref = Pin::as_mut(&mut self.0);
let next = mut_ref.iter().as_mut()?.next()?; let next = mut_ref.iter().as_mut()?.next()?;
let res = match 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())), Ok((k, v)) => Ok((k.to_vec(), v.to_vec())),
}; };
Some(res) Some(res)
@@ -453,9 +459,9 @@ where
fn tx_iter_item<'a>( fn tx_iter_item<'a>(
item: std::result::Result<(&'a [u8], &'a [u8]), heed::Error>, 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())) item.map(|(k, v)| (k.to_vec(), v.to_vec()))
.map_err(|e| TxOpError(Error::from(e))) .map_err(DbError::from)
} }
// ---- utility ---- // ---- utility ----
+10 -9
View File
@@ -1,6 +1,6 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::{Db, Error, Result}; use crate::{Db, DbError, Error, Result};
/// List of supported database engine types /// List of supported database engine types
/// ///
@@ -49,14 +49,14 @@ impl std::str::FromStr for Engine {
"lmdb" | "heed" => Ok(Self::Lmdb), "lmdb" | "heed" => Ok(Self::Lmdb),
"sqlite" | "sqlite3" | "rusqlite" => Ok(Self::Sqlite), "sqlite" | "sqlite3" | "rusqlite" => Ok(Self::Sqlite),
"fjall" => Ok(Self::Fjall), "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())), "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(Error( kind => Err(DbError(
format!( format!(
"Invalid DB engine: {} (options are: lmdb, sqlite, fjall)", "Invalid DB engine: {} (options are: lmdb, sqlite, fjall)",
kind kind
) )
.into(), .into(),
)), ).into()),
} }
} }
} }
@@ -72,22 +72,23 @@ pub fn open_db(path: &PathBuf, engine: Engine, opt: &OpenOpt) -> Result<Db> {
match engine { match engine {
// ---- Sqlite DB ---- // ---- Sqlite DB ----
#[cfg(feature = "sqlite")] #[cfg(feature = "sqlite")]
Engine::Sqlite => crate::sqlite_adapter::open_db(path, opt), Engine::Sqlite => Ok(crate::sqlite_adapter::open_db(path, opt)?),
// ---- LMDB DB ---- // ---- LMDB DB ----
#[cfg(feature = "lmdb")] #[cfg(feature = "lmdb")]
Engine::Lmdb => crate::lmdb_adapter::open_db(path, opt), Engine::Lmdb => Ok(crate::lmdb_adapter::open_db(path, opt)?),
// ---- Fjall DB ---- // ---- Fjall DB ----
#[cfg(feature = "fjall")] #[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 // 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 // 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. // support for one or more engines by default.
#[allow(unreachable_patterns)] #[allow(unreachable_patterns)]
engine => Err(Error( engine => Err(DbError(
format!("DB engine support not available in this build: {}", engine).into(), format!("DB engine support not available in this build: {}", engine).into(),
)), )
.into()),
} }
} }
+67 -50
View File
@@ -12,7 +12,7 @@ use rusqlite::{params, Rows, Statement, Transaction};
use crate::{ use crate::{
open::{Engine, OpenOpt}, 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, TxResult, TxValueIter, Value, ValueIter,
}; };
@@ -20,7 +20,7 @@ pub use rusqlite;
// ---- top-level open function // ---- 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()); info!("Opening Sqlite database at: {}", path.display());
let manager = r2d2_sqlite::SqliteConnectionManager::file(path); let manager = r2d2_sqlite::SqliteConnectionManager::file(path);
SqliteDb::open(manager, opt.fsync) SqliteDb::open(manager, opt.fsync)
@@ -32,21 +32,33 @@ type Connection = r2d2::PooledConnection<SqliteConnectionManager>;
// --- err // --- err
impl From<rusqlite::Error> for DbError {
fn from(e: rusqlite::Error) -> DbError {
DbError(format!("Sqlite: {}", e).into())
}
}
impl From<rusqlite::Error> for Error { impl From<rusqlite::Error> for Error {
fn from(e: rusqlite::Error) -> 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 { impl From<r2d2::Error> for Error {
fn from(e: r2d2::Error) -> Error { fn from(e: r2d2::Error) -> Error {
Error(format!("Sqlite: {}", e).into()) DbError::from(e).into()
} }
} }
impl From<rusqlite::Error> for TxOpError { impl From<rusqlite::Error> for TxOpError {
fn from(e: rusqlite::Error) -> TxOpError { fn from(e: rusqlite::Error) -> TxOpError {
TxOpError(e.into()) DbError::from(e).into()
} }
} }
@@ -62,7 +74,7 @@ pub struct SqliteDb {
} }
impl 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| { let manager = manager.with_init(move |db| {
db.pragma_update(None, "journal_mode", "WAL")?; db.pragma_update(None, "journal_mode", "WAL")?;
if sync_mode { if sync_mode {
@@ -82,16 +94,16 @@ impl SqliteDb {
} }
impl SqliteDb { impl SqliteDb {
fn get_tree(&self, i: usize) -> Result<Arc<str>> { fn get_tree(&self, i: usize) -> DbResult<Arc<str>> {
self.trees self.trees
.read() .read()
.unwrap() .unwrap()
.get(i) .get(i)
.cloned() .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 stmt = db.prepare(&format!("SELECT v FROM {} WHERE k = ?1", tree))?;
let mut res_iter = stmt.query([key])?; let mut res_iter = stmt.query([key])?;
match res_iter.next()? { match res_iter.next()? {
@@ -106,7 +118,7 @@ impl IDb for SqliteDb {
format!("sqlite3 v{} (using rusqlite crate)", rusqlite::version()) 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 name = format!("tree_{}", name.replace(':', "_COLON_"));
let mut trees = self.trees.write().unwrap(); 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 mut trees = vec![];
let db = self.db.get()?; let db = self.db.get()?;
@@ -150,13 +162,13 @@ impl IDb for SqliteDb {
Ok(trees) Ok(trees)
} }
fn snapshot(&self, base_path: &Path) -> Result<()> { fn snapshot(&self, base_path: &Path) -> DbResult<()> {
std::fs::create_dir_all(base_path)?; std::fs::create_dir_all(base_path)?;
let path = Engine::Sqlite let path = Engine::Sqlite
.db_path(base_path) .db_path(base_path)
.into_os_string() .into_os_string()
.into_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); info!("Start sqlite VACUUM INTO `{}`", path);
self.db.get()?.execute("VACUUM INTO ?1", params![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)?; let tree = self.get_tree(tree)?;
self.internal_get(&self.db.get()?, &tree, key) 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 tree = self.get_tree(tree)?;
let db = self.db.get()?; 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) 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 tree = self.get_tree(tree)?;
let db = self.db.get()?; let db = self.db.get()?;
let lock = self.write_lock.lock(); let lock = self.write_lock.lock();
@@ -206,7 +218,7 @@ impl IDb for SqliteDb {
Ok(()) Ok(())
} }
fn remove(&self, tree: usize, key: &[u8]) -> Result<()> { fn remove(&self, tree: usize, key: &[u8]) -> DbResult<()> {
let tree = self.get_tree(tree)?; let tree = self.get_tree(tree)?;
let db = self.db.get()?; let db = self.db.get()?;
let lock = self.write_lock.lock(); let lock = self.write_lock.lock();
@@ -217,7 +229,7 @@ impl IDb for SqliteDb {
Ok(()) Ok(())
} }
fn clear(&self, tree: usize) -> Result<()> { fn clear(&self, tree: usize) -> DbResult<()> {
let tree = self.get_tree(tree)?; let tree = self.get_tree(tree)?;
let db = self.db.get()?; let db = self.db.get()?;
let lock = self.write_lock.lock(); let lock = self.write_lock.lock();
@@ -228,13 +240,13 @@ impl IDb for SqliteDb {
Ok(()) Ok(())
} }
fn iter(&self, tree: usize) -> Result<ValueIter<'_>> { fn iter(&self, tree: usize) -> DbResult<ValueIter<'_>> {
let tree = self.get_tree(tree)?; let tree = self.get_tree(tree)?;
let sql = format!("SELECT k, v FROM {} ORDER BY k ASC", tree); let sql = format!("SELECT k, v FROM {} ORDER BY k ASC", tree);
DbValueIterator::make(self.db.get()?, &sql, []) 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 tree = self.get_tree(tree)?;
let sql = format!("SELECT k, v FROM {} ORDER BY k DESC", tree); let sql = format!("SELECT k, v FROM {} ORDER BY k DESC", tree);
DbValueIterator::make(self.db.get()?, &sql, []) DbValueIterator::make(self.db.get()?, &sql, [])
@@ -245,7 +257,7 @@ impl IDb for SqliteDb {
tree: usize, tree: usize,
low: Bound<&'r [u8]>, low: Bound<&'r [u8]>,
high: Bound<&'r [u8]>, high: Bound<&'r [u8]>,
) -> Result<ValueIter<'_>> { ) -> DbResult<ValueIter<'_>> {
let tree = self.get_tree(tree)?; let tree = self.get_tree(tree)?;
let (bounds_sql, params) = bounds_sql(low, high); let (bounds_sql, params) = bounds_sql(low, high);
@@ -263,7 +275,7 @@ impl IDb for SqliteDb {
tree: usize, tree: usize,
low: Bound<&'r [u8]>, low: Bound<&'r [u8]>,
high: Bound<&'r [u8]>, high: Bound<&'r [u8]>,
) -> Result<ValueIter<'_>> { ) -> DbResult<ValueIter<'_>> {
let tree = self.get_tree(tree)?; let tree = self.get_tree(tree)?;
let (bounds_sql, params) = bounds_sql(low, high); let (bounds_sql, params) = bounds_sql(low, high);
@@ -300,9 +312,9 @@ impl IDb for SqliteDb {
} }
TxFnResult::DbErr => { TxFnResult::DbErr => {
tx.tx.rollback().map_err(Error::from).map_err(TxError::Db)?; tx.tx.rollback().map_err(Error::from).map_err(TxError::Db)?;
Err(TxError::Db(Error( Err(TxError::Db(
"(this message will be discarded)".into(), DbError("(this message will be discarded)".into()).into(),
))) ))
} }
}; };
@@ -320,15 +332,15 @@ struct SqliteTx<'a> {
} }
impl<'a> 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(|| { 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(), "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 let mut stmt = self
.tx .tx
.prepare(&format!("SELECT v FROM {} WHERE k = ?1", tree))?; .prepare(&format!("SELECT v FROM {} WHERE k = ?1", tree))?;
@@ -341,11 +353,11 @@ impl<'a> SqliteTx<'a> {
} }
impl<'a> ITx for 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)?; let tree = self.get_tree(tree)?;
self.internal_get(tree, key) 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 tree = self.get_tree(tree)?;
let mut stmt = self.tx.prepare(&format!("SELECT COUNT(*) FROM {}", tree))?; let mut stmt = self.tx.prepare(&format!("SELECT COUNT(*) FROM {}", tree))?;
let mut res_iter = stmt.query([])?; 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 tree = self.get_tree(tree)?;
let sql = format!("INSERT OR REPLACE INTO {} (k, v) VALUES (?1, ?2)", tree); let sql = format!("INSERT OR REPLACE INTO {} (k, v) VALUES (?1, ?2)", tree);
self.tx.execute(&sql, params![key, value])?; self.tx.execute(&sql, params![key, value])?;
Ok(()) 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)?; let tree = self.get_tree(tree)?;
self.tx self.tx
.execute(&format!("DELETE FROM {} WHERE k = ?1", tree), params![key])?; .execute(&format!("DELETE FROM {} WHERE k = ?1", tree), params![key])?;
Ok(()) Ok(())
} }
fn clear(&mut self, tree: usize) -> TxOpResult<()> { fn clear(&mut self, tree: usize) -> DbResult<()> {
let tree = self.get_tree(tree)?; let tree = self.get_tree(tree)?;
self.tx.execute(&format!("DELETE FROM {}", tree), [])?; self.tx.execute(&format!("DELETE FROM {}", tree), [])?;
Ok(()) Ok(())
} }
fn iter(&self, tree: usize) -> TxOpResult<TxValueIter<'_>> { fn iter(&self, tree: usize) -> DbResult<TxValueIter<'_>> {
let tree = self.get_tree(tree)?; let tree = self.get_tree(tree)?;
let sql = format!("SELECT k, v FROM {} ORDER BY k ASC", tree); let sql = format!("SELECT k, v FROM {} ORDER BY k ASC", tree);
TxValueIterator::make(self, &sql, []) 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 tree = self.get_tree(tree)?;
let sql = format!("SELECT k, v FROM {} ORDER BY k DESC", tree); let sql = format!("SELECT k, v FROM {} ORDER BY k DESC", tree);
TxValueIterator::make(self, &sql, []) TxValueIterator::make(self, &sql, [])
@@ -389,7 +401,7 @@ impl<'a> ITx for SqliteTx<'a> {
tree: usize, tree: usize,
low: Bound<&'r [u8]>, low: Bound<&'r [u8]>,
high: Bound<&'r [u8]>, high: Bound<&'r [u8]>,
) -> TxOpResult<TxValueIter<'_>> { ) -> DbResult<TxValueIter<'_>> {
let tree = self.get_tree(tree)?; let tree = self.get_tree(tree)?;
let (bounds_sql, params) = bounds_sql(low, high); let (bounds_sql, params) = bounds_sql(low, high);
@@ -407,7 +419,7 @@ impl<'a> ITx for SqliteTx<'a> {
tree: usize, tree: usize,
low: Bound<&'r [u8]>, low: Bound<&'r [u8]>,
high: Bound<&'r [u8]>, high: Bound<&'r [u8]>,
) -> TxOpResult<TxValueIter<'_>> { ) -> DbResult<TxValueIter<'_>> {
let tree = self.get_tree(tree)?; let tree = self.get_tree(tree)?;
let (bounds_sql, params) = bounds_sql(low, high); let (bounds_sql, params) = bounds_sql(low, high);
@@ -426,15 +438,20 @@ impl<'a> ITx for SqliteTx<'a> {
// complicated, they must hold the Statement and Row objects // complicated, they must hold the Statement and Row objects
// therefore quite some unsafe code (it is a self-referential struct) // therefore quite some unsafe code (it is a self-referential struct)
struct DbValueIterator<'a> { struct DbValueIterator {
db: Connection, db: Connection,
stmt: Option<Statement<'a>>, // These two are not really static (they are actually self referential :o)
iter: Option<Rows<'a>>, stmt: Option<Statement<'static>>,
iter: Option<Rows<'static>>,
_pin: PhantomPinned, _pin: PhantomPinned,
} }
impl<'a> DbValueIterator<'a> { impl DbValueIterator {
fn make<P: rusqlite::Params>(db: Connection, sql: &str, args: P) -> Result<ValueIter<'a>> { fn make<'res, P: rusqlite::Params>(
db: Connection,
sql: &str,
args: P,
) -> DbResult<ValueIter<'res>> {
let res = DbValueIterator { let res = DbValueIterator {
db, db,
stmt: None, stmt: None,
@@ -468,7 +485,7 @@ impl<'a> DbValueIterator<'a> {
} }
} }
impl<'a> Drop for DbValueIterator<'a> { impl Drop for DbValueIterator {
fn drop(&mut self) { fn drop(&mut self) {
trace!("drop iter"); trace!("drop iter");
drop(self.iter.take()); drop(self.iter.take());
@@ -476,10 +493,10 @@ impl<'a> Drop for DbValueIterator<'a> {
} }
} }
struct DbValueIteratorPin<'a>(Pin<Box<DbValueIterator<'a>>>); struct DbValueIteratorPin(Pin<Box<DbValueIterator>>);
impl<'a> Iterator for DbValueIteratorPin<'a> { impl Iterator for DbValueIteratorPin {
type Item = Result<(Value, Value)>; type Item = DbResult<(Value, Value)>;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let mut_ref = Pin::as_mut(&mut self.0); let mut_ref = Pin::as_mut(&mut self.0);
@@ -504,7 +521,7 @@ impl<'a> TxValueIterator<'a> {
tx: &'a SqliteTx<'a>, tx: &'a SqliteTx<'a>,
sql: &str, sql: &str,
args: P, args: P,
) -> TxOpResult<TxValueIter<'a>> { ) -> DbResult<TxValueIter<'a>> {
let stmt = tx.tx.prepare(sql)?; let stmt = tx.tx.prepare(sql)?;
let res = TxValueIterator { let res = TxValueIterator {
stmt, stmt,
@@ -538,7 +555,7 @@ impl<'a> Drop for TxValueIterator<'a> {
struct TxValueIteratorPin<'a>(Pin<Box<TxValueIterator<'a>>>); struct TxValueIteratorPin<'a>(Pin<Box<TxValueIterator<'a>>>);
impl<'a> Iterator for TxValueIteratorPin<'a> { impl<'a> Iterator for TxValueIteratorPin<'a> {
type Item = TxOpResult<(Value, Value)>; type Item = DbResult<(Value, Value)>;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let mut_ref = Pin::as_mut(&mut self.0); let mut_ref = Pin::as_mut(&mut self.0);
+241
View File
@@ -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,
}
}
+1 -1
View File
@@ -39,8 +39,8 @@ backtrace.workspace = true
bytes.workspace = true bytes.workspace = true
bytesize.workspace = true bytesize.workspace = true
chrono.workspace = true chrono.workspace = true
fundu-systemd.workspace = true
timeago.workspace = true timeago.workspace = true
parse_duration.workspace = true
hex.workspace = true hex.workspace = true
tracing.workspace = true tracing.workspace = true
tracing-subscriber.workspace = true tracing-subscriber.workspace = true
+1 -1
View File
@@ -49,7 +49,7 @@ pub struct OpenLmdbOpt {
pub(crate) fn do_conversion(args: ConvertDbOpt) -> Result<()> { pub(crate) fn do_conversion(args: ConvertDbOpt) -> Result<()> {
if args.input_engine == args.output_engine { 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 { let opt = OpenOpt {

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