feat: serve lib decoupling - #218
Conversation
…BackendRegistry Backends were wired in by link-time collection (the inventory crate): each backend submitted six registration statics that the linker only preserved if the binary happened to reference the backend crate. That is an invisible, compiles-fine failure mode -- a backend added as an unreferenced dependency silently isn't there at runtime. Introduce an explicit BackendRegistry in extenddb-storage that holds the six per-backend factories (bootstrapper, storage-config deserializer, operations engine, settings store, diagnostics store, server components), installed once into a process-global OnceLock via set_registry(). The six lookup free functions now resolve against the installed registry; their signatures are unchanged, so every call site is untouched. A missing registry degrades to the existing unknown-backend error rather than a panic. Each backend exposes a single register(&mut BackendRegistry) instead of six inventory::submit! blocks; extenddb-storage-postgres::register() is the reference. main() builds the registry, registers the compiled-in backend, and installs it before dispatch. The inventory dependency is dropped from both crates. Behavior-preserving: workspace unit tests green, clippy -D warnings + fmt clean. Signed-off-by: Lee Hannigan <lhnng@amazon.com>
…xtenddb-config crate
serve() cannot be a library entrypoint while AppConfig lives in the bin crate.
Move the configuration surface (AppConfig and subsections, load(), redaction
helpers, expand_tilde, build_config_entries, PID-file path helpers) out of
crates/bin into a new extenddb-config crate that depends only on the
extenddb-storage trait surface -- never on a concrete backend. This gives both
extenddb-server (for the upcoming serve()) and the CLI a shared lower crate to
depend on without a server<->app cycle.
The crate is backend-agnostic: the two dead postgres-gated items
(StorageConfig Default and default_backend, which had no callers) are dropped,
and StorageConfig deserialization no longer defaults to "postgres" -- the
[storage] backend key is now required, matching the decision that core carries
no built-in default backend. pid_file_path{,_default} move here from
serve_helpers so the server crate can write the PID file without depending on
the bin.
bin now depends on extenddb-config; all config call sites are unchanged via a
"use extenddb_config as config" alias. The now-unused external config crate
dependency is dropped from bin.
Behavior-preserving: workspace unit tests green (incl. new config tests),
clippy -D warnings + fmt clean.
Signed-off-by: Lee Hannigan <lhnng@amazon.com>
…o extenddb-server The server orchestration (component assembly, cache wiring, AppState construction, worker spawning, TLS assembly, PID-file cleanup) lived in the bin crate, so a third party holding only their backend crate plus the published extenddb-* library crates could not run a server without forking bin. Move that orchestration into extenddb-server as a public `serve(config, listener, port, run_dir, foreground, git_hash)` entrypoint, and move the generic background workers (log-level poll, throttling poll, metrics prune/flush, login-attempt cleanup, capacity warning) into the server crate alongside it. AppState/start_server/Router/caches already lived here, so this completes the assembly into one library call. Build provenance (git hash) is passed in by the caller rather than read via env!(): the deployed binary knows its own provenance, and the library must not depend on the bin's build.rs environment variables. log_to_syslog_raw moves into the server crate with serve; the bin retains only the CLI concerns (config permission check, arg parsing, banner, bind, daemonize, PID dir). bin's cmd_serve::run now loads config, binds, daemonizes, then calls extenddb_server::serve. The unused syslog-tracing dependency is dropped from bin. Behavior-preserving: workspace unit tests green, clippy -D warnings + fmt clean. Signed-off-by: Lee Hannigan <lhnng@amazon.com>
…es thin bin Completes the library decoupling. The full CLI (serve, init, destroy, verify, migrate, status, stop, settings, manage, catalog-check) plus its dispatch, subcommand modules, and helpers move out of crates/bin into a new, backend-agnostic extenddb-app crate exposing `run(BuildInfo)`. crates/bin is now the reference thin bin: it registers exactly one backend (postgres), installs the registry, and calls extenddb_app::run. This is the copy-paste template for a third-party backend author — swap the register call, supply your own build provenance, ship an extenddb-<backend> image, and touch no ExtendDB core crate. First-party and third-party backends now follow the identical path; there is no privileged wiring for postgres. Build provenance (git hash, build time) is passed in via BuildInfo rather than read through env!(), since the app library cannot see the bin's build.rs environment. cmd_serve::run takes git_hash and forwards it to extenddb_server::serve. The bin's dependency set collapses to extenddb-app, extenddb-storage, the backend crate, and anyhow. Behavior-preserving: workspace unit tests green (incl. the serve arg-parsing tests now in extenddb-app), clippy -D warnings + fmt clean. Signed-off-by: Lee Hannigan <lhnng@amazon.com>
Keep crates/config/src/lib.rs under the 500-line file limit (552 -> 440) by moving REDACTED_CONFIG_KEYS, redact_if_sensitive, and build_config_entries into a new display module; build_config_entries is re-exported so the public path is unchanged.
inventory was replaced by the explicit BackendRegistry and is no longer used by crates/storage or crates/storage-postgres.
a6cacab to
a738a7a
Compare
There was a problem hiding this comment.
Clean, well-motivated refactoring. The new crate topology makes sense and the inventory → explicit BackendRegistry migration eliminates a class of silent linker failures. Commit structure is excellent — each commit is independently reviewable and behavior-preserving.
One discussion point: serve() signature — git_hash: &str
The current signature is:
pub async fn serve(
app_config: config::AppConfig,
std_listener: TcpListener,
port: u16,
run_dir: String,
foreground: bool,
git_hash: &str,
) -> anyhow::Result<()>Since this is a public library API, the git_hash lifetime is worth a quick discussion. All realistic callers pass env!("EXTENDDB_GIT_HASH") which is 'static, so &'static str would be the tighter contract. On the other hand, &str is strictly more permissive today and doesn't break anyone — it only becomes a problem if you later want to narrow to &'static str (breaking change) or if the function needs to store it beyond the call (would need String/'static).
Not blocking — happy to discuss offline to sort out what makes sense here
Minor non-blocking observations (no action needed now)
portis derivable fromstd_listener.local_addr()— passing both creates a consistency coupling- 6 positional params approaching ergonomic threshold; a
ServeConfigstruct would be more future-proof BackendRegistry::register_*silently overwrites on duplicate key — atracing::warnwould help catch wiring bugsREDACTED_CONFIG_KEYScomment says "keep in sync" — consider exporting from one place
Everything else looks solid. Nice work.
Non-blocking observations (9 items)All of these are suggestions for future improvement — none should block merge. 1.
|
register_* used HashMap::insert, so a second backend claiming an existing name silently won and the effective backend depended on the order of register calls in main. A tracing::warn would be invisible here because registration runs before the subscriber is installed, so record the collisions and fail set_registry instead — the error surfaces through main's ? before any request is served. RegistryAlreadySet is replaced by RegistryError, which carries either the already-installed case or the list of colliding (slot, backend) pairs. Adds unit tests for the duplicate and distinct-name paths. Signed-off-by: Lee Hannigan <lhnng@amazon.com>
The console settings page carried its own copy of the redaction patterns and display.rs carried a "keep in sync" comment, which is a manual-sync hazard: a pattern added in one place silently leaks values in the other. Export should_redact from extenddb-config and have the console import it. The server crate already depends on config, so the two lists cannot drift apart any more. Signed-off-by: Lee Hannigan <lhnng@amazon.com>
main.rs calls extenddb_storage_postgres::register unconditionally, so --no-default-features did not build, and a bin with the feature disabled would register no backend at all — every command would then fail at runtime with an unknown-backend error. The thin bin exists to wire exactly one backend, so the dependency is not optional. Making it non-optional removes configuration that cannot work rather than adding a cfg gate around the only call that makes the binary useful. Signed-off-by: Lee Hannigan <lhnng@amazon.com>
serve() took six positional parameters, two of which encoded caller
concerns the library should not know:
- port duplicated std_listener, forcing the caller to keep them
consistent. It is now read back from the bound listener, which also
resolves an ephemeral (port 0) bind correctly.
- foreground described the deployment model. Replaced by LogTarget
{Syslog, Stderr}, so the library only decides where logs go. The PID
file is now written unconditionally: the value daemonize writes for the
grandchild is the same as std::process::id() post-fork, so this is a
consistent rewrite and no longer needs a foreground branch.
- git_hash: &str is now BuildInfo.git_hash: &'static str. Every caller
already passes a compile-time env!, and declaring the real lifetime now
means the value can later be stored past the call without a breaking
signature change.
The remaining arguments move into a #[non_exhaustive] ServeParams built
via new() + with_log_target(), so later fields are non-breaking.
BuildInfo moves to the server crate (app re-exports it) and gains version,
read from the bin crate. The banner and console version string previously
used the library crate's CARGO_PKG_VERSION, which reports the wrong number
as soon as crate versions stop moving together.
Background workers now take a CancellationToken and stop at their next
tick, and serve awaits them (bounded at 5s) after the HTTP server stops.
Previously they were abandoned to runtime drop, so the metrics flush
worker could lose its final bucket; it now performs a full drain on the
way out. ServerRuntimeHooks::spawn_workers returns its JoinHandles so
backend workers join the same drain, and extenddb-storage exports
CancellationToken plus a sleep_or_shutdown helper so a backend crate needs
no tokio-util dependency of its own.
Also removes the duplicate raw-syslog writer: the app's panic hook now
calls the server crate's log_to_syslog_raw instead of its own inline
unsafe block, and drops the now-empty registry comment section left by the
inventory removal.
Verified: 674 unit tests and 408 Rust integration tests pass; live SIGTERM
drains all 13 workers (6 generic + 7 postgres) in 38ms.
Signed-off-by: Lee Hannigan <lhnng@amazon.com>
The cancellation behaviour was only verified by hand, so a regression to
loop { sleep } would not have failed anything.
Three tests against fake stores:
- worker_keeps_running_until_cancelled — guards the two below from passing
vacuously for a worker that returns immediately and never does work.
- cancellation_stops_a_sleeping_worker — a worker mid-sleep on an hour-long
interval returns within 2s of cancellation, which is only possible if it
also selects on the token.
- cancellation_flushes_the_final_partial_bucket — the flush interval is 60s,
so a persisted row proves the write came from the cancellation drain and
not a periodic tick.
Both drain tests were mutation checked: restoring drain_age to
FLUSH_INTERVAL fails the final-flush test, and reverting the cleanup worker
to loop { sleep } fails the cancellation test.
Corrects a count in 2b9ab1b's message: the pre-existing unit-test total was
617, not 674; it is 620 with these three.
Signed-off-by: Lee Hannigan <lhnng@amazon.com>
|
Thanks Paul, this was a useful list. All nine are addressed. Pushed as 1, 2 (port redundancy, param count).
3 (no Your point about flushing final metrics was correct: Covered by three tests in 4 ( 5 (vestigial 6 (silent overwrite on duplicate name). Registration runs before the tracing subscriber exists, so a 7 (stale 8 (duplicate 9 ( Two things I have not resolved. The 5s drain bound stacks on the existing graceful window, so a worker wedged in a slow backend call could extend shutdown; the happy path is 38ms but I have not measured the pathological case against a 10s container grace period. Happy to lower the bound if you think that matters. Verification: 620 unit tests and 408 Rust integration tests pass, clippy clean with no suppressions, and each of the five commits compiles standalone. |
Design question: registry vs. single-backend setterThe Would a simpler single-backend design work equally well? // In extenddb-storage:
pub struct Backend {
pub bootstrapper: BootstrapperFactory,
pub storage_config: StorageConfigDeserializer,
pub operations: &'static dyn OperationsEngine,
pub settings_store: SettingsStoreFactory,
pub diagnostics_store: DiagnosticsStoreFactory,
pub server_components: ServerComponentsFactory,
}
pub fn set_backend(backend: Backend) -> Result<(), BackendAlreadySet> { ... }Thin bin becomes: fn main() {
extenddb_storage::set_backend(extenddb_storage_postgres::backend());
extenddb_app::run(...)
}What this would give you:
What you'd lose:
Not blocking — the registry as implemented is clean and the extra complexity is bounded (~100 lines). This is more of a "is the HashMap dispatch earning its keep?" question. If there's a concrete plan for multi-backend binaries or config-time selection, the registry makes sense. If every deployment is always one-backend-per-binary anyway, Curious whether RFC #168 considered this alternative or specifically chose multi-backend support for a reason I'm not seeing. |
…name-keyed registry
A binary is built for exactly one backend, so the registry's string-keyed
dispatch was capability the project does not use. It also allowed a class of
runtime error that cannot exist without it: a mistyped or absent backend name
produced an "unknown backend" failure after startup rather than a compile-time
guarantee.
extenddb_storage::Backend collects the six factories a backend provides, and
set_backend installs it once from the thin bin. The six lookup functions drop
their backend-name parameter and read the installed backend directly, so the
name no longer flows through 13 call sites. BackendRegistry, RegistryError and
the duplicate-registration detection are gone: with one backend there is no
collision to detect.
Backend carries its own name, which keeps the config file format unchanged.
Configuration asks the installed backend for its [storage.<name>] section
rather than taking that name from the file, so:
* the [storage] backend key is now optional again, restoring the original
"omit it and get the compiled-in backend" behaviour that this branch had
regressed into a hard error;
* when the key is present it is validated against the compiled-in backend and
a mismatch fails at startup naming the correct binary, instead of being
silently accepted and failing later.
The BackendNotFound/UnknownBackend error variants are renamed
BackendNotInstalled, which is the only remaining failure mode.
Verified: 620 unit tests, 408 Rust integration tests, fmt and clippy clean.
Live checks: a config with no backend key starts, a config naming a different
backend is rejected with an actionable message, and extenddb version reports
the single compiled-in backend.
Signed-off-by: Lee Hannigan <lhnng@amazon.com>
…ndDB#218 main Rebase onto upstream main after PR ExtendDB#218 (serve lib decoupling), which replaced inventory backend registration with an explicit set_backend/Backend model and split the CLI into extenddb-app. Also adapts to backup-trait and worker changes and to new backup_arn_scoping conformance tests pulled in by the rebase. - Replace the six inventory::submit! blocks with a single extenddb_storage_mongodb::backend() constructor plus a server_components_factory fn, mirroring the postgres backend. - Drop the now-removed inventory dependency. - Feature-gate the thin bin: install the mongodb backend under --features mongodb, else postgres. - Scope describe_backup and delete_backup to account_id (added to the BackupEngine trait upstream); exclude DELETED backups from describe_backup so a deleted backup reads as BackupNotFoundException. - Give backup ARNs a timestamp-plus-8-hex-char random id so they are not guessable from creation time alone. - Return the spawned worker JoinHandles from spawn_workers, whose trait signature now requires Vec<JoinHandle<()>>.
…cert SANs Make the binary supervisable and probeable from a container runtime. First of two changes for container readiness; the migration concurrency guard follows separately. - healthcheck: new subcommand that probes /health over HTTPS and exits 0 or 1, so a Docker HEALTHCHECK needs no shell or curl and works on distroless. It reports liveness, which is what a HEALTHCHECK and a Kubernetes livenessProbe want: /health is a static handler that does not query the backend, and a liveness probe that failed on a database outage would restart every replica at once. There is no readiness endpoint yet; adding one backed by a cached storage-layer round-trip is the follow-up. Connect, read, and write are bounded at 3s (TcpStream::connect has no timeout of its own), every resolved address is tried so a name resolving to both ::1 and 127.0.0.1 works, and --endpoint accepts an optional scheme, port, path, and IPv6 literal. The flagless probe derives its host from the configured bind_addr rather than assuming 127.0.0.1, so an IPv6-bound server is not reported unhealthy, and --port mirrors serve's own override. Read and write timeout failures are propagated, since a bounded probe is the whole point of the command. - serve --foreground: write no PID file and skip the run directory by default, so the container can use a read-only root filesystem. Daemon mode is unchanged. With no PID file to read, `stop` now probes the port and reports that a server is listening under foreign supervision rather than claiming nothing is running; `status` already degrades to an unknown PID. - serve --write-pid-file: opt back into the PID file in foreground mode, for shell use and tooling that wants `stop` and `status` to work. It goes to the same run_dir path daemon mode uses, so neither command needs extra arguments, and run_dir then has to be writable. Ignored in daemon mode, which always writes one. devtools/run-tests restarts the server with `stop` to apply a config change, so the integration workflow passes this flag; without it that restart silently did nothing: `stop` failed, `serve` could not bind, and the health check passed against the process that was never replaced. That path no longer suppresses errors either, so a failed restart fails the run instead of reporting success. - init --tls-san <name> (repeatable): append Subject Alternative Names to the generated self-signed certificate so it is valid for the name clients use, such as an in-cluster service DNS name, not just localhost/127.0.0.1/bind-addr. Values are trimmed and de-duplicated case-insensitively. init never regenerates an existing certificate, so a later --tls-san cannot take effect; rather than exit 0 having dropped the name and leave clients to hit a TLS hostname verification failure, it verifies the existing certificate covers every requested SAN and fails with an actionable error otherwise. Certificate generation moved ahead of all database work so a bad SAN fails before any state is created. The coverage check uses rustls's own `verify_server_name`, so it adds no new dependency. A wildcard such as *.svc.cluster.local is a valid certificate entry but not a valid server name, so coverage is tested by substituting a single label; without that, a wildcard accepted on the first run failed on every later one, which is a crash loop for the idempotent entrypoint. Every requested name is validated before generation, so a malformed wildcard fails on the first run rather than the next. - docs: correct the architecture and deployment guides, which claimed the server always daemonizes and that foreground mode still writes a PID file, and replace the container recipe that waited on that PID file with a foreground entrypoint plus a HEALTHCHECK. - tests: add tests/test_cli_container_readiness.py covering SAN generation, dedup/blank handling, the not-covered failure and the already-covered idempotent case, healthcheck up/down/--endpoint plus prompt failure against an unreachable host, and foreground leaving no PID file or run directory while still exiting on SIGTERM. Runs under an isolated $HOME so the suite no longer overwrites the developer's real ~/.extenddb certificate. - devtools/run-tests: exclude the new file from the main pytest suite and run it in the CLI section instead, alongside test_cli_lifecycle.py. Like those tests it starts and stops its own servers and creates its own databases, so it cannot run in parallel against the shared instance the main suite uses. Rebased onto the post-#218 layout: the CLI now lives in crates/app, so cmd_healthcheck joins it there. ServeParams gains pid_file: Option<PathBuf> in place of run_dir, which it only ever used to derive that path, so serve() no longer writes a PID file unconditionally and needs no notion of a run directory.
…ndDB#218 main Rebase onto upstream main after PR ExtendDB#218 (serve lib decoupling), which replaced inventory backend registration with an explicit set_backend/Backend model and split the CLI into extenddb-app. Also adapts to backup-trait and worker changes and to new backup_arn_scoping conformance tests pulled in by the rebase. - Replace the six inventory::submit! blocks with a single extenddb_storage_mongodb::backend() constructor plus a server_components_factory fn, mirroring the postgres backend. - Drop the now-removed inventory dependency. - Feature-gate the thin bin: install the mongodb backend under --features mongodb, else postgres. - Scope describe_backup and delete_backup to account_id (added to the BackupEngine trait upstream); exclude DELETED backups from describe_backup so a deleted backup reads as BackupNotFoundException. - Give backup ARNs a timestamp-plus-8-hex-char random id so they are not guessable from creation time alone. - Return the spawned worker JoinHandles from spawn_workers, whose trait signature now requires Vec<JoinHandle<()>>.
…isory lock Two replicas running `extenddb migrate` at once race each other: both evaluate which migrations are pending before either records anything, both apply, and one fails with a duplicate pg_type_typname_nsp_index key when the concurrent CREATE TABLE IF NOT EXISTS statements collide in PostgreSQL's system catalog. That makes an idempotent container entrypoint, which runs migrate on every start of every replica, unsafe. Take a namespaced session-level advisory lock around the migration step so migrators serialize. The second blocks, then finds the schema already applied and no-ops. The lock is held on a dedicated connection to the catalog database for the duration and released on every path, including the "nothing to do" early return and error returns; if the process dies, PostgreSQL releases it when the connection closes. - A migrator that must wait tries the lock first and prints why it is waiting, instead of sitting silent for as long as the other migration takes. - Acquiring is not re-entrant: a second acquire would open a second connection and block on the lock the first holds, deadlocking against itself, so it is rejected. A failed unlock is reported rather than swallowed, though closing the connection releases the lock anyway so it is never fatal. - Advisory locks are scoped to a database, so migrators serialize only if they share a catalog database — they do, since it comes from the same connection string. - After acquiring, verify against pg_locks that this session holds the lock, and fail hard if it does not. A transaction-pooling proxy (pgbouncer in transaction mode, RDS Proxy) puts the lock on an arbitrary pooled backend, which otherwise leaves migrators unserialized while looking safe. One statement maps to one backend even through such a proxy, so the check detects it. Direct RDS and Aurora connections pass. - init takes the same lock around its own schema work, so it cannot race a migrate running on another replica. Two concurrent inits cannot reach the migrations at all: the second aborts earlier at create_catalog_db because the database already exists, so this guards the narrower init-versus-migrate overlap. - The Bootstrapper trait gains acquire_migration_lock and release_migration_lock with default no-ops, so out-of-tree backends compile unchanged. A new MinimalBootstrapper test implements only the required methods, so it stops compiling if a defaulted method loses its default, and pins object safety. - tests: add tests/test_cli_migrate_concurrency.py asserting that two concurrent `migrate --yes` runs both succeed with exactly one applying the migration and the other observing it as done, and that a migrate blocked on a lock held from an external session waits rather than proceeding, then completes and reports that it waited. Both tests fail when the lock is disabled. - devtools/run-tests: exclude the new file from the main pytest suite and run it in the CLI section instead, alongside test_cli_lifecycle.py. Like those tests it starts and stops its own servers and creates its own databases, so it cannot run in parallel against the shared instance the main suite uses. Rebased onto the post-#218 layout: cmd_migrate and cmd_init now live in crates/app. The guard is unaffected by that move and by the registry removal; sqlx::migrate (#221) is not yet adopted, so the custom migration runner this serializes is still in place.
What
Decouples the ExtendDB server into library crates so a backend can be added crate-only, with no edits to any core crate. Four commits:
refactor(storage)replaceinventorylink-time registration with an explicitBackendRegistryinstalled once into a process-globalOnceLock(set_registry). Backends now expose oneregister(&mut BackendRegistry)instead of sixinventory::submit!blocks. Lookup free-function signatures are unchanged, so all call sites are untouched.refactor(config)extractAppConfig+ loading into a new backend-agnosticextenddb-configcrate (breaks the would-beserver ↔ appcycle).refactor(server)promote the server orchestration to a publicextenddb_server::serve(config, listener, port, run_dir, foreground, git_hash); move the generic background workers intoextenddb-server.refactor(app)lift the full CLI + dispatch into a newextenddb-appcrate exposingrun(BuildInfo);crates/binbecomes the Postgres thin bin and the reference template for backend authors.First-party and third-party backends now follow the identical
serve(config, registry)path, no privileged wiring for Postgres. Out of scope: the open-once/shared-handle lifecycle contract (only exclusive-lock embedded engines need it; no-op for Postgres/SQLite).Why
The server orchestration (component assembly, cache wiring,
AppState, worker spawning, TLS, PID lifecycle) lived in thebincrate, and backend registration relied oninventorylink-time collection. A third party with only their backend crate + the publishedextenddb-*libraries could not run a server without forkingbin. This makes the server a library call so a backend is a crate plus a ~10-linemain.Closes #168
Testing done
cargo build --workspace,cargo fmt --all -- --check,cargo clippy --workspace --all-targets -- -D warnings(the CI gate): all clean.cargo test --workspace: 585 passed, 0 failed (unit + doc).integration.yml): comprehensive 326 passed / 0 failed; rust-integration transaction/idempotency/GSI/data-plane/management paths pass. The only integration failures are pre-existingcapacity_throttlingcases that are environment-sensitive locally and green in CI.version,serve+/health, a SigV4-signed CreateTable/PutItem/GetItem/DeleteTable round-trip, and every CLI subcommand —init,migrate,settings,manage,catalog-check,verify,status,stop,destroy.mainthrough the identical local harness fails on exactly the same environment-sensitive tests, confirming the remaining local failures are not introduced by this change.cargo clippy -- -W clippy::pedanticreports warnings, but they are pre-existing project-wide style (missing# Errorsdocs, backticks, function length) present onmain; this PR's moves don't add net-new pedantic findings.Checklist
cargo test --workspace)cargo fmt --check)cargo clippy --all-targets -- -D warningspasses (-W clippy::pedanticemits only pre-existing project-wide warnings, unchanged by this PR)pid_file_pathtest inextenddb-config)backendchange (below) needs a note in the config reference / sample docs; flagging as a follow-upStoragetrait, auth model, on-disk format, or public CLI surface, an RFC has been accepted or is linked below. Otherwise, an ADR captures the decision (link below).ADR / RFC: RFC #168 (serve/library decoupling + registry). Public CLI surface is unchanged; the new public library surface (
serve,BackendRegistry) is governed by RFC #168.Breaking changes
StorageConfigdeserialization no longer defaults the backend to"postgres". The[storage] backendkey is now required, a backend-agnostic config crate cannot bake in a default. Configs that setbackendexplicitly (all shipped/sample configs do) are unaffected; a config omitting it now gets a clear error instead of a silent Postgres default.By submitting this pull request, I confirm that my contribution is made under
the terms of the Apache License 2.0 and I agree to the Developer Certificate of
Origin (DCO). See CONTRIBUTING.md for details.