Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

S3

dynamic-config-s3 reads configuration from an S3 object — on AWS, or on anything speaking its API. The AWS SDK is async throughout, so this implements AsyncRemoteSource.

[dependencies]
dynamic-config = { version = "<version>", features = ["async"] }
dynamic-config-s3 = "<version>"
#![allow(unused)]
fn main() {
use dynamic_config_s3::S3;

DbConfig::set_remote_async(S3::new("myapp-config", "prod/db.json").await?);
DbConfig::refresh_remote_async().await?;
}

What it reads: one object whose body is a whole configuration document; format from the key's extension, or with_format.

Several objects, one document: Keys::several(["prod/base.json", "prod/local.json"]) merges in call order — later wins — and Keys::prefix("prod/") folds every object under the prefix into one document, refusing an overlap between two of them. Neither is atomic and S3 offers nothing that would make one so: there is no batch read, so a list is one GetObject per key and a prefix is one ListObjectsV2 and then one GetObject each. AWS made listings strongly consistent in December 2020, so the listing itself is no longer the hole it was — but another implementation of this API is free not to be, and the gap between the listing and the reads is there in every case.

The 512-key bound is applied to the listing, not to what it fetched: a page is asked for one key more than the budget allows, so a prefix pointed at a whole bucket is refused after one request rather than after a million bodies. Keys the store answers with are checked against the literal prefix, and the zero-byte "folder" object a console leaves behind is skipped.

Credentials: through aws-config — the chain every AWS tool uses (environment, shared profile, instance role, task role, IRSA). Deliberately not re-implemented: a second credential chain in a program that already has one is a bug waiting for a rotation.

Not only AWS: with_config takes an SdkConfig the program built, which is how MinIO, Ceph, Cloudflare R2 and Backblaze B2 are reached — path-style addressing is forced, because virtual-hosted buckets need DNS only AWS has. The test suite runs against MinIO, so this is checked rather than claimed.

TLS as data, with one gap: S3::with_tls(&config, bucket, key, tls) takes a private certificate authority — which is what MinIO, Ceph and a company's own gateway actually need — but not a client certificate. The AWS SDK's TLS context is a trust store and nothing else, so mTLS is refused rather than ignored, pointing at from_client. The CA is parsed here purely in order to refuse: the SDK's connector calls .expect("cert parsable") on it, so an unreadable certificate would otherwise be a panic at the first connection. TLS, and the one vocabulary all eight speak

Watching: S3 cannot push without a notification pipeline (SNS, SQS, EventBridge), and that is a deployment's decision rather than a library's — so watch polls, and says so. It does not download the object every tick: HEAD returns the ETag, so an unchanged configuration costs one small request and no transfer — which matters on a bucket that charges per gigabyte. The ETag is taken from the read, not the check before it, so a write landing between the two is not delivered twice. A multi-key source refuses to be watched: an ETag belongs to an object, and a set of objects has none.

A failing poll says so: S3::new(bucket, key).await?.reporting_to(sink) takes the same remote_sink() the loop already pushes documents through, and reports the failures inside the loop to it — a HEAD that did not answer, and a GET that did not answer after the ETag moved. Surviving a failure is what makes this necessary: a poll loop that retries forever is a loop that reports nothing forever, so dynamic_config_remote_up reports the last delivery rather than the last attempt, and a bucket that stopped answering on Tuesday is indistinguishable from a configuration nobody has changed since Tuesday. A reported failure moves the failure streak and nothing else, so dynamic_config_remote_last_fetch_seconds keeps ageing while dynamic_config_remote_up goes to zero — which is the pair an alert wants: down, and stale for how long. Only the failure's kind and key path are recorded; the bucket, the key and the endpoint stay out of it.

What is deliberately not reported is a refusal at the door — no format, or a source naming several keys — because watch() returns those to the caller standing there, before there is a loop to be silent in, and they are deployment mistakes rather than a store that stopped answering.

The README carries the full story; MSRV 1.88.