diff --git a/crates/wasmeld-console/src/api.rs b/crates/wasmeld-console/src/api.rs index c6cdde5..049d4f0 100644 --- a/crates/wasmeld-console/src/api.rs +++ b/crates/wasmeld-console/src/api.rs @@ -1,4 +1,14 @@ //! Axum management and public gateway protocol adapters. +//! +//! The management router accepts JSON and multipart packages and exposes +//! lifecycle operations. The gateway router is intentionally smaller: it +//! accepts raw `application/octet-stream` input and returns raw bytes plus +//! `x-wasmeld-revision` and `x-wasmeld-latency-ms` response headers. +//! +//! Neither router installs authentication or authorization middleware. A +//! production deployment must place the appropriate identity, authorization, +//! TLS, rate-limit, and request-size policy in front of these routers. CORS is +//! a browser access policy and must not be treated as authentication. use super::*; use axum::{ @@ -21,6 +31,11 @@ use wasmeld_package::wit_package::WitPackageError; /// /// The returned router exposes Runtime control, component registration, /// invocation, events, and WIT Registry endpoints. +/// +/// `allowed_origins` only affects browser CORS responses. Passing an empty +/// vector does not make the API private; it merely omits allowed origins. +/// Multipart bodies receive one MiB of framing headroom above the larger +/// configured artifact limit. pub fn app(console: Arc, allowed_origins: Vec) -> Router { let max_body_bytes = console .max_artifact_bytes() @@ -89,7 +104,18 @@ pub fn app(console: Arc, allowed_origins: Vec) -> Router { /// Builds the public data-plane API around the same resident Runtime. /// /// This router intentionally contains no registration, lifecycle, Registry, -/// or deployment management routes. +/// or deployment management routes. Callers must send +/// `Content-Type: application/octet-stream`; the stable service ID is resolved +/// to its active immutable revision at the start of each request. +/// +/// # Wire example +/// +/// ```text +/// POST /v1/services/image-resize/invoke +/// Content-Type: application/octet-stream +/// +/// +/// ``` pub fn gateway_app(console: Arc) -> Router { let max_input_bytes = console.max_gateway_input_bytes(); Router::new() @@ -126,6 +152,9 @@ async fn gateway_invoke( let input = body.map_err(|_| GatewayError::PayloadTooLarge { limit: console.max_gateway_input_bytes(), })?; + // Deployment resolution and Actor enqueue happen in the same blocking + // closure. The returned revision header therefore identifies the exact + // revision that received this request, even if deployment changes later. let invocation = run_blocking(console, move |console| { console.gateway_invoke(&service_id, input.to_vec()) }) @@ -262,6 +291,9 @@ async fn publish_wit_package( State(console): State>, mut multipart: Multipart, ) -> Result<(StatusCode, Json), ApiError> { + // Axum materializes this one field in memory. DefaultBodyLimit above is + // only a transport guard; WitRegistry repeats the exact artifact limit and + // validates the package identity before committing it to disk. let mut package = None; while let Some(field) = multipart .next_field() @@ -453,6 +485,9 @@ where // an operation may have changed state before returning its error. let operation_console = Arc::clone(&console); let result = run_blocking(operation_console, operation).await; + // The flush marker orders previously accepted metric deltas before the + // full snapshot transaction. It means "attempted", not "durably written"; + // the telemetry worker logs write failures independently. console.flush_invocation_telemetry().await; let persisted = console.persist_snapshot().await; diff --git a/crates/wasmeld-console/src/invocation_persistence.rs b/crates/wasmeld-console/src/invocation_persistence.rs index 22a13ab..bd8e30e 100644 --- a/crates/wasmeld-console/src/invocation_persistence.rs +++ b/crates/wasmeld-console/src/invocation_persistence.rs @@ -1,4 +1,10 @@ //! Bounded, asynchronous persistence for data-plane invocation telemetry. +//! +//! Invocation responses must not wait for libSQL, so producers use +//! [`tokio::sync::mpsc::Sender::try_send`] and one task writes ordered batches. +//! When the queue is full or closed, the update is deliberately dropped and a +//! warning is emitted. These counters and events are operational telemetry; +//! they are not suitable as a billing ledger or audit log. use std::sync::Arc; @@ -10,6 +16,10 @@ const QUEUE_CAPACITY: usize = 1024; const MAX_BATCH_SIZE: usize = 64; /// Non-blocking producer used by Wasmtime invocation threads. +/// +/// All producers share one FIFO queue. Queue order is preserved for accepted +/// records, but a process crash can lose records that have not yet reached +/// libSQL. pub(crate) struct InvocationPersistence { sender: mpsc::Sender, } @@ -21,6 +31,10 @@ enum Command { impl InvocationPersistence { /// Starts the single ordered writer on the current Tokio runtime. + /// + /// `sync` is shared with full Console snapshot persistence. Taking it + /// around each batch prevents a snapshot transaction from racing a metric + /// delta transaction. pub(crate) fn start(persistence: Persistence, sync: Arc>) -> Self { let (sender, mut receiver) = mpsc::channel(QUEUE_CAPACITY); tokio::spawn(async move { @@ -45,6 +59,10 @@ impl InvocationPersistence { tracing::error!(%error, "failed to persist invocation telemetry"); } } + // Flush is an ordering barrier, not a durability receipt. A + // database error above is logged rather than returned, and a + // flush may wait for records dequeued after the marker when + // those records fit in the same batch. for flush in flushes { let _ = flush.send(()); } @@ -54,6 +72,9 @@ impl InvocationPersistence { } /// Enqueues telemetry without extending API response latency. + /// + /// Saturation drops this update instead of applying backpressure to the + /// Component invocation path. pub(crate) fn record(&self, update: InvocationUpdate) { if let Err(error) = self.sender.try_send(Command::Record(update)) { tracing::warn!(%error, "invocation telemetry queue is unavailable; update dropped"); @@ -61,6 +82,10 @@ impl InvocationPersistence { } /// Waits until all telemetry queued before this call has been attempted. + /// + /// This method is used before a control-plane snapshot so the snapshot + /// reads the newest in-memory counters after older delta writes. It does + /// not report persistence failures; those are visible through tracing. pub(crate) async fn flush(&self) { let (sender, receiver) = oneshot::channel(); if self.sender.send(Command::Flush(sender)).await.is_ok() { diff --git a/crates/wasmeld-console/src/kv_backend.rs b/crates/wasmeld-console/src/kv_backend.rs index e8278a9..e99c050 100644 --- a/crates/wasmeld-console/src/kv_backend.rs +++ b/crates/wasmeld-console/src/kv_backend.rs @@ -1,4 +1,18 @@ //! Bounded bridge between synchronous Component Host calls and async Toasty. +//! +//! Wasmtime calls [`wasmeld_runtime::KvBackend`] synchronously on an Actor +//! thread, while Toasty and libSQL are asynchronous. This adapter moves every +//! database operation onto one dedicated current-thread Tokio runtime. The +//! bounded channel prevents a slow database from accumulating unbounded work +//! or blocking every Actor while waiting for queue capacity. +//! +//! # Timeout semantics +//! +//! A timeout only stops the caller from waiting. It does not remove a command +//! that is already queued or cancel a libSQL operation that has started. +//! Consequently, `set` and `delete` may commit after the Component observes +//! `KvBackendError::Timeout`. Components should make retries idempotent and +//! must not interpret a timeout as proof that no mutation occurred. use std::{ fmt, io, @@ -15,6 +29,9 @@ use wasmeld_runtime::{KvBackend, KvBackendError}; use crate::persistence::Persistence; const COMMAND_CAPACITY: usize = 64; +// Keep Host calls responsive even when a Component requests a larger timeout. +// The Component-facing deadline is an upper bound, not permission to occupy an +// Actor thread indefinitely. const MAX_RESPONSE_TIMEOUT: Duration = Duration::from_millis(400); pub(crate) struct ToastyKvBackend { @@ -47,6 +64,11 @@ enum KvCommand { } impl ToastyKvBackend { + /// Starts the database worker and waits until its Tokio runtime is ready. + /// + /// Waiting for the ready signal makes construction fail deterministically + /// instead of returning a backend whose first operation discovers that + /// the worker could not initialize. pub(crate) fn start(persistence: Persistence) -> io::Result { let (sender, receiver) = mpsc::sync_channel(COMMAND_CAPACITY); let (ready_sender, ready_receiver) = mpsc::sync_channel(1); @@ -78,6 +100,8 @@ impl ToastyKvBackend { build: impl FnOnce(SyncSender>) -> KvCommand, ) -> Result { let (response, receiver) = mpsc::sync_channel(1); + // `try_send` is intentional: waiting for queue capacity here would + // stall the Actor and defeat the Runtime's invocation deadline. match self.inner.sender.try_send(build(response)) { Ok(()) => {} Err(TrySendError::Full(_)) => return Err(KvBackendError::Busy), @@ -149,6 +173,9 @@ impl KvBackend for ToastyKvBackend { impl Drop for BackendInner { fn drop(&mut self) { + // Only the final Arc owner reaches this Drop. Joining guarantees the + // worker no longer holds the Persistence handle when Console shuts + // down. If commands precede Shutdown, the FIFO channel drains them. let _ = self.sender.send(KvCommand::Shutdown); if let Ok(worker) = self.worker.get_mut() && let Some(worker) = worker.take() @@ -163,6 +190,8 @@ fn run_worker( receiver: Receiver, ready: SyncSender>, ) { + // Toasty is driven from one thread so synchronous Host calls never need to + // enter or block the Console server's multi-thread Tokio runtime. let runtime = match tokio::runtime::Builder::new_current_thread() .enable_all() .build() @@ -175,6 +204,9 @@ fn run_worker( }; let _ = ready.send(Ok(())); + // A single consumer also defines the ordering of mutations submitted to + // this backend. Ordering across different Wasmeld processes is delegated + // to libSQL and is not guaranteed by this queue. while let Ok(command) = receiver.recv() { match command { KvCommand::Get { diff --git a/crates/wasmeld-console/src/lib.rs b/crates/wasmeld-console/src/lib.rs index 7b8c244..ca3513d 100644 --- a/crates/wasmeld-console/src/lib.rs +++ b/crates/wasmeld-console/src/lib.rs @@ -5,6 +5,32 @@ //! to libSQL through Toasty. It also hosts an immutable filesystem-backed WIT //! package Registry. Actor memory remains process-local and is never presented //! as durable state. +//! +//! # Ownership model +//! +//! * [`Console`] owns the in-process Runtime and serializes control-plane +//! mutations with a lifecycle mutex. +//! * libSQL stores desired service/deployment state and bounded telemetry. +//! * Component and WIT bytes are immutable filesystem artifacts addressed by +//! identity and digest. +//! * On restart, Actors are recreated from persisted manifests; their linear +//! memory is intentionally reset. +//! +//! # Example +//! +//! ```no_run +//! use wasmeld_console::{Console, ConsoleConfig}; +//! +//! # async fn example() -> Result<(), Box> { +//! let console = Console::new(ConsoleConfig::default()).await?; +//! for service in console.services()? { +//! println!("{}@{}: {:?}", service.id, service.revision, service.status); +//! } +//! # Ok(()) +//! # } +//! ``` + +#![warn(missing_docs)] mod api; mod invocation_persistence; @@ -82,6 +108,10 @@ impl Default for ConsoleConfig { /// registered service metadata, artifacts, metrics, and events remain managed /// by the Console. pub struct Console { + // Lock order for lifecycle mutations is `lifecycle` -> `runtime` -> + // `state`. Invocation does not take `lifecycle`; it briefly reads Runtime + // and then updates state after the Actor call. Keep new code on this order + // to avoid deadlocks between HTTP control operations. runtime: RwLock>, runtime_config: RuntimeConfig, artifact_dir: PathBuf, @@ -124,26 +154,37 @@ struct DeploymentRecord { #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub enum ServiceStatus { + /// Actor is accepting invocations for this revision. Running, + /// Revision is registered but has no active Actor. Stopped, + /// Its most recent Actor trapped and must be started again. Faulted, } /// API projection of one public service deployment. #[derive(Clone, Debug, Serialize)] pub struct DeploymentView { + /// Stable public service ID used by the Gateway route. pub service_id: String, + /// Immutable revision currently selected for the service. pub active_revision: String, + /// Current lifecycle status of the selected revision. pub status: ServiceStatus, + /// Unix timestamp of the most recent deployment change. pub updated_at_ms: u64, } /// Exact versioned host interface imported by a registered Component. #[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct CapabilityView { + /// Fully qualified versioned WIT interface. pub interface: String, + /// WIT package identity without the version suffix. pub package: String, + /// Interface name inside the package. pub name: String, + /// Exact semantic version linked by the Runtime. pub version: String, } @@ -161,16 +202,27 @@ impl From for CapabilityView { /// API projection of a registered service and its current metrics. #[derive(Clone, Debug, Serialize)] pub struct ServiceView { + /// Stable service ID. pub id: String, + /// Immutable Component revision. pub revision: String, + /// Canonical path of the stored Component artifact. pub artifact: String, + /// Fully versioned world declared by the package. pub world: String, + /// Exact Host interfaces discovered from Component imports. pub capabilities: Vec, + /// Current Actor status for this revision. pub status: ServiceStatus, + /// Unix timestamp of the most recent lifecycle change. pub updated_at_ms: u64, + /// Platform limits injected during registration. pub limits: ResourceLimits, + /// Persisted number of completed invocation attempts. pub calls: u64, + /// Persisted number of failed invocation attempts. pub errors: u64, + /// Latency of the most recently persisted invocation. pub last_latency_ms: Option, } @@ -195,11 +247,17 @@ impl ServiceRecord { /// One persisted Console lifecycle or invocation event. #[derive(Clone, Debug, Serialize)] pub struct EventView { + /// Process-independent monotonically increasing event identity. pub id: u64, + /// Event creation time as Unix milliseconds. pub timestamp_ms: u64, + /// Stable category used by API consumers. pub kind: EventKind, + /// Related service ID, or `None` for Runtime-wide events. pub service_id: Option, + /// Related immutable revision when the event targets one version. pub revision: Option, + /// Human-readable operational detail; not a machine protocol. pub message: String, } @@ -207,12 +265,19 @@ pub struct EventView { #[derive(Clone, Copy, Debug, Serialize)] #[serde(rename_all = "snake_case")] pub enum EventKind { + /// A Component package was registered. Registered, + /// An inactive revision and its artifacts were removed. Unregistered, + /// A public deployment switched to a revision. Deployed, + /// Runtime or Actor startup completed. Started, + /// Runtime or Actor shutdown completed. Stopped, + /// One data-plane or management invocation completed. Invoked, + /// A lifecycle or invocation operation failed. Failed, } @@ -348,55 +413,76 @@ struct GatewayInvocation { /// Errors raised by Console state, persistence, Registry, or Runtime operations. #[derive(Debug, Error)] pub enum ConsoleError { + /// API input failed semantic validation. #[error("{0}")] InvalidRequest(String), + /// The requested registered service revision does not exist. #[error("service revision {0} is not managed by Wasmeld")] ServiceNotFound(ServiceKey), + /// The public service has no selected revision. #[error("service {0} has no active deployment")] DeploymentNotFound(String), + /// Unregister was attempted on the revision currently serving traffic. #[error("service revision {0} is the active deployment")] ActiveDeployment(ServiceKey), + /// An operation requiring Wasmtime was attempted while Runtime is stopped. #[error("Wasmeld Runtime is not running")] RuntimeNotRunning, + /// Uploaded package bytes exceed the configured control-plane limit. #[error("artifact exceeds the {limit}-byte limit")] - ArtifactTooLarge { limit: usize }, + ArtifactTooLarge { + /// Maximum accepted upload bytes. + limit: usize, + }, + /// Component package decoding or integrity validation failed. #[error(transparent)] Package(#[from] PackageError), + /// WIT Registry storage or integrity validation failed. #[error(transparent)] WitRegistry(#[from] WitRegistryError), + /// Component artifact or manifest filesystem access failed. #[error("failed to access {path}: {source}")] Storage { + /// Path involved in the failed operation. path: PathBuf, + /// Underlying filesystem failure. #[source] source: io::Error, }, + /// A generated Runtime manifest could not be encoded. #[error("failed to serialize manifest: {0}")] ManifestSerialize(#[from] toml::ser::Error), + /// Toasty or libSQL control-state persistence failed. #[error("database operation failed: {0}")] Database(#[from] toasty::Error), + /// The synchronous-to-async KV worker thread could not start. #[error("failed to start persistent KV backend: {0}")] KvBackendStart(#[source] io::Error), + /// Persisted rows contain an unknown enum, invalid key, or invalid revision. #[error("database contains invalid console data: {0}")] InvalidDatabaseData(String), + /// Component compilation, Actor lifecycle, or invocation failed. #[error(transparent)] Runtime(#[from] RuntimeError), + /// A blocking Runtime operation panicked or was cancelled. #[error("blocking runtime task failed: {0}")] Join(#[from] tokio::task::JoinError), + /// An internal lifecycle, state, or Runtime lock was poisoned. #[error("console state lock was poisoned")] LockPoisoned, } @@ -793,6 +879,11 @@ impl Console { }; manifest.validate()?; + // Artifacts are committed before Runtime registration so + // `register_from_file` can recover the same bytes after restart. + // Every later fallible step removes files it created until the Runtime + // owns the registration. The HTTP wrapper persists the state snapshot + // after this method returns. write_atomic(&component_path, &component_bytes)?; let manifest_toml = toml::to_string_pretty(&manifest)?; if let Err(error) = write_atomic(&manifest_path, manifest_toml.as_bytes()) { @@ -950,6 +1041,10 @@ impl Console { let result = runtime.invoke(key, input); let latency_ms = duration_ms(started_at.elapsed()); + // Update counters synchronously so management reads immediately + // observe this call. Durable telemetry is queued below and is + // intentionally best-effort: queue saturation or a process crash can + // lose recent counters/events. Do not use these values for billing. let mut state = self.state()?; let record = state .services @@ -995,6 +1090,9 @@ impl Console { service_id: &str, input: Vec, ) -> Result { + // Clone the selected revision while holding state, then release the + // lock before entering Runtime. An activation racing this call affects + // only subsequent resolutions; this call remains pinned to `revision`. let revision = { let state = self.state()?; state @@ -1139,7 +1237,9 @@ impl Console { async fn persist_snapshot(&self) -> Result<(), ConsoleError> { // Serialize snapshots so a slower earlier write cannot overwrite a - // newer state captured by a concurrent HTTP operation. + // newer state captured by a concurrent HTTP operation. This mutex is + // also shared with the invocation delta writer, which prevents the two + // transaction styles from interleaving. let _sync = self.persistence_sync.lock().await; let (services, events, deployments) = self.persistence_snapshot()?; self.persistence.sync(services, events, deployments).await?; diff --git a/crates/wasmeld-console/src/main.rs b/crates/wasmeld-console/src/main.rs index 6658788..ac2d6cf 100644 --- a/crates/wasmeld-console/src/main.rs +++ b/crates/wasmeld-console/src/main.rs @@ -1,4 +1,25 @@ //! Standalone Wasmeld control-plane and data-plane server entry point. +//! +//! Configuration is supplied through environment variables: +//! +//! - `WASMELD_ADDR` binds the management API (default `127.0.0.1:8080`). +//! - `WASMELD_GATEWAY_ADDR` binds the public byte API (default `0.0.0.0:8081`). +//! - `WASMELD_ARTIFACT_DIR`, `WASMELD_WIT_REGISTRY_DIR`, and +//! `WASMELD_DATABASE_PATH` select durable local storage. +//! - `WASMELD_ALLOWED_ORIGINS` is a comma-separated management UI CORS list. +//! - `RUST_LOG` configures tracing. +//! +//! For example: +//! +//! ```text +//! WASMELD_ADDR=127.0.0.1:8080 \ +//! WASMELD_GATEWAY_ADDR=0.0.0.0:8081 \ +//! RUST_LOG=wasmeld_console=debug \ +//! cargo run -p wasmeld-console +//! ``` +//! +//! The defaults expose only the gateway on non-loopback interfaces. +//! Authentication and TLS must be supplied by the deployment environment. use std::{env, net::SocketAddr, path::PathBuf, sync::Arc}; @@ -45,6 +66,8 @@ async fn main() -> Result<(), Box> { let gateway = gateway_app(Arc::clone(&console)); let management_listener = tokio::net::TcpListener::bind(management_address).await?; let gateway_listener = tokio::net::TcpListener::bind(gateway_address).await?; + // Both servers share one shutdown edge so neither listener remains alive + // after Ctrl+C has begun process termination. let (shutdown_sender, shutdown_receiver) = tokio::sync::watch::channel(false); let signal_task = tokio::spawn(async move { shutdown_signal().await; @@ -61,6 +84,9 @@ async fn main() -> Result<(), Box> { ); signal_task.abort(); result?; + // Axum has stopped accepting requests at this point. Flush accepted + // telemetry before dropping Console; persistence failures are traced by + // the worker because telemetry is an operational, best-effort stream. console.flush_invocation_telemetry().await; Ok(()) } diff --git a/crates/wasmeld-console/src/persistence.rs b/crates/wasmeld-console/src/persistence.rs index fc592d8..551758a 100644 --- a/crates/wasmeld-console/src/persistence.rs +++ b/crates/wasmeld-console/src/persistence.rs @@ -3,6 +3,15 @@ //! Component bytes and WIT packages remain filesystem artifacts. This module //! persists service manifests, deployments, metrics, the bounded event //! history, and service-scoped Host KV entries. +//! +//! There are two write paths: +//! +//! - [`Persistence::sync`] stores a complete control-plane snapshot. +//! - [`Persistence::record_invocations`] applies ordered metric deltas. +//! +//! Callers serialize these paths with the Console persistence mutex. Existing +//! service counters are intentionally owned by the delta path; a snapshot +//! updates their manifest and timestamp without replacing those counters. use std::{ collections::BTreeSet, @@ -55,6 +64,10 @@ pub(crate) struct StoredKvEntry { } /// One invocation's durable metric delta and event. +/// +/// The caller assigns the event ID before enqueueing the update. A missing +/// service row is tolerated because unregister may race queued telemetry; the +/// event is still retained for diagnostics. pub(crate) struct InvocationUpdate { pub service_key: String, pub failed: bool, @@ -64,6 +77,10 @@ pub(crate) struct InvocationUpdate { } /// Serialized access to the Toasty database connection. +/// +/// This mutex protects a single Toasty [`Db`] handle. It is separate from the +/// higher-level Console persistence mutex, which coordinates whole snapshot +/// and invocation-delta operations before they acquire this connection. #[derive(Clone)] pub(crate) struct Persistence { db: Arc>, @@ -131,6 +148,11 @@ impl Persistence { } /// Atomically upserts a Console snapshot and prunes expired events. + /// + /// The input is authoritative for services and deployments: rows omitted + /// from those collections are removed. Events older than the oldest + /// supplied retained event are pruned. Existing invocation counters are + /// preserved because [`Self::record_invocations`] owns counter changes. pub async fn sync( &self, services: Vec, @@ -178,6 +200,8 @@ impl Persistence { } } + // Console stores events newest-first, so the last element is the + // oldest ID that should survive snapshot pruning. if let Some(oldest_event) = events.last() { StoredEvent::filter(StoredEvent::fields().id().lt(oldest_event.id)) .delete() @@ -217,6 +241,9 @@ impl Persistence { } /// Atomically records invocation deltas and their bounded event history. + /// + /// All supplied updates commit together. Metrics use saturating arithmetic + /// so corrupted or extremely old databases cannot wrap counters. pub(crate) async fn record_invocations( &self, updates: Vec, @@ -268,6 +295,8 @@ impl Persistence { service_id: &str, entry_key: &str, ) -> toasty::Result>> { + // The namespace is the stable service ID rather than its revision. + // Deploying a new revision therefore preserves application state. let mut db = self.db.lock().await; Ok( StoredKvEntry::filter_by_service_id_and_entry_key(service_id, entry_key) @@ -284,6 +313,8 @@ impl Persistence { entry_key: String, value: Vec, ) -> toasty::Result<()> { + // Upsert makes a retry after an ambiguous Host timeout idempotent for + // the same `(service_id, entry_key, value)`. let mut db = self.db.lock().await; StoredKvEntry::upsert_by_service_id_and_entry_key(service_id, entry_key) .value(value) diff --git a/crates/wasmeld-console/src/wit_registry.rs b/crates/wasmeld-console/src/wit_registry.rs index de2c4b6..318e0ca 100644 --- a/crates/wasmeld-console/src/wit_registry.rs +++ b/crates/wasmeld-console/src/wit_registry.rs @@ -37,28 +37,40 @@ struct StoredWitPackage { /// Errors produced by WIT Registry storage and integrity checks. #[derive(Debug, Error)] pub enum WitRegistryError { + /// Uploaded binary WIT exceeds the configured byte limit. #[error("WIT package exceeds the {limit}-byte limit")] - TooLarge { limit: usize }, + TooLarge { + /// Maximum accepted artifact bytes. + limit: usize, + }, + /// The immutable `name@version` already exists. #[error("WIT package {0} is already published and cannot be overwritten")] AlreadyExists(String), + /// No indexed artifact matches the requested identity. #[error("WIT package {0} was not found")] NotFound(String), + /// Uploaded or persisted bytes are not a valid versioned binary WIT package. #[error(transparent)] InvalidPackage(#[from] WitPackageError), + /// Directory layout and decoded artifact identity disagree. #[error("invalid WIT registry data: {0}")] InvalidStorage(String), + /// Registry directory or artifact filesystem access failed. #[error("failed to access {path}: {source}")] Storage { + /// Path involved in the failed operation. path: PathBuf, + /// Underlying filesystem failure. #[source] source: io::Error, }, + /// The process-local Registry index lock was poisoned. #[error("WIT registry lock was poisoned")] LockPoisoned, }