plugin/host/src/backend.rs
//! **The one `RepositoryBackendTrait` implementation.**
//!
//! There is exactly one in this crate, and both a linked handler and a loaded
//! `.wasm` reach the router through it. That is not a convention that has to be
//! checked — `fast_routes.rs` receives an `Arc<dyn RepositoryBackendTrait>`
//! whose concrete type is [`HandlerBackend`] in both cases, so there is no
//! second code path for it to take. If the router ever needed to know which kind
//! it had, the design would be wrong.
//!
//! The split that makes this possible is [`HandlerRuntime`]: *how* a verb is
//! executed (a direct call, or an ABI round trip through wasmtime) versus *what*
//! the router asks for. Storage is bound into the runtime at construction, so by
//! the time the trait is implemented the two are the same shape.
use std::sync::Arc;
use holger_plugin_abi::{
BlobStore, PackageHandler, WireArtifactEntry, WireArtifactId, WireHttpRequest,
WireHttpResponse, WireManifest,
};
use traits::{ArtifactEntry, ArtifactFormat, ArtifactId, RepositoryBackendTrait};
use crate::convert::{entry_from_wire, format_from_wire, id_from_wire, id_to_wire};
/// A [`BlobStore`] usable from the server's request path.
pub type SharedStore = Arc<dyn BlobStore + Send + Sync>;
/// One handler, with its storage already bound — however it is executed.
///
/// The verbs mirror [`PackageHandler`] minus the `store` argument: a runtime owns
/// its store, because a wasm module's store has to live in the wasmtime `Store`
/// data and cannot be handed in per call.
pub trait HandlerRuntime: Send + Sync {
fn manifest(&self) -> &WireManifest;
fn fetch(&self, id: &WireArtifactId) -> Result<Option<Vec<u8>>, String>;
fn put(&self, id: &WireArtifactId, data: &[u8]) -> Result<(), String>;
fn list(
&self,
name_filter: Option<&str>,
limit: usize,
) -> Result<Vec<WireArtifactEntry>, String>;
fn coordinate_for_path(&self, suburl: &str) -> Option<WireArtifactId>;
fn http(&self, req: &WireHttpRequest) -> Result<WireHttpResponse, String>;
}
/// A handler compiled into the server, bound to a store. The **native** half of
/// "every package handler can be written native or WASM".
pub struct LinkedHandler<H: PackageHandler + Send + Sync> {
handler: H,
store: SharedStore,
manifest: WireManifest,
}
impl<H: PackageHandler + Send + Sync> LinkedHandler<H> {
pub fn new(handler: H, store: SharedStore) -> Self {
let manifest = handler.manifest();
Self {
handler,
store,
manifest,
}
}
}
impl<H: PackageHandler + Send + Sync> HandlerRuntime for LinkedHandler<H> {
fn manifest(&self) -> &WireManifest {
&self.manifest
}
fn fetch(&self, id: &WireArtifactId) -> Result<Option<Vec<u8>>, String> {
self.handler.fetch(&*self.store, id)
}
fn put(&self, id: &WireArtifactId, data: &[u8]) -> Result<(), String> {
self.handler.put(&*self.store, id, data)
}
fn list(
&self,
name_filter: Option<&str>,
limit: usize,
) -> Result<Vec<WireArtifactEntry>, String> {
self.handler.list(&*self.store, name_filter, limit)
}
fn coordinate_for_path(&self, suburl: &str) -> Option<WireArtifactId> {
self.handler.coordinate_for_path(suburl)
}
fn http(&self, req: &WireHttpRequest) -> Result<WireHttpResponse, String> {
self.handler.http(&*self.store, req)
}
}
/// A package handler as holger's router sees it.
///
/// `Debug` prints the repository name, the format and the handler name — never
/// the runtime, because the whole point is that a caller cannot tell a linked
/// handler from a loaded module by looking at one.
pub struct HandlerBackend {
/// The repository name this backend is wired under — the router's key, which
/// is an operator's choice and NOT the handler's name (two repositories can
/// run the same handler over different stores).
repo_name: String,
format: ArtifactFormat,
runtime: Box<dyn HandlerRuntime>,
}
impl HandlerBackend {
/// Wrap a runtime. Fails if the handler declared a format holger does not
/// know — never falling back to `Raw`, because a mis-declared format that
/// registered anyway would serve real bytes under the wrong protocol.
pub fn new(
repo_name: impl Into<String>,
runtime: Box<dyn HandlerRuntime>,
) -> Result<Self, String> {
let format = format_from_wire(&runtime.manifest().format)?;
Ok(Self {
repo_name: repo_name.into(),
format,
runtime,
})
}
/// The handler's own name (`skidbladnir`), as distinct from the repository
/// name this backend is mounted at.
pub fn handler_name(&self) -> &str {
&self.runtime.manifest().handler
}
pub fn into_backend(self) -> Arc<dyn RepositoryBackendTrait> {
Arc::new(self)
}
}
impl std::fmt::Debug for HandlerBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HandlerBackend")
.field("repo_name", &self.repo_name)
.field("format", &self.format)
.field("handler", &self.runtime.manifest().handler)
.finish()
}
}
impl RepositoryBackendTrait for HandlerBackend {
fn name(&self) -> &str {
&self.repo_name
}
fn format(&self) -> ArtifactFormat {
self.format.clone()
}
fn is_writable(&self) -> bool {
self.runtime.manifest().writable
}
fn fetch(&self, id: &ArtifactId) -> anyhow::Result<Option<traits::Bytes>> {
self.runtime
.fetch(&id_to_wire(id))
.map(|o| o.map(traits::Bytes::from))
.map_err(|e| anyhow::anyhow!("{}: {e}", self.repo_name))
}
fn put(&self, id: &ArtifactId, data: &[u8]) -> anyhow::Result<()> {
self.runtime
.put(&id_to_wire(id), data)
.map_err(|e| anyhow::anyhow!("{}: {e}", self.repo_name))
}
fn list(&self, name_filter: Option<&str>, limit: usize) -> anyhow::Result<Vec<ArtifactEntry>> {
let rows = self
.runtime
.list(name_filter, limit)
.map_err(|e| anyhow::anyhow!("{}: {e}", self.repo_name))?;
Ok(rows.into_iter().map(entry_from_wire).collect())
}
/// A handler lists, and its `coordinate_for_path` is mandatory in the ABI
/// (the inverse of its own path scheme), so the mapping is total. No
/// archive (a handler has no znippy handle) and no delete (this backend
/// cannot vouch for the store's digest recomputation).
fn caps(&self) -> traits::Caps {
traits::Caps {
listing: true,
path_mapping: true,
..traits::Caps::NONE
}
}
fn handle_http2_request(
&self,
method: &str,
suburl: &str,
body: &[u8],
) -> anyhow::Result<traits::HttpReply> {
let req = WireHttpRequest {
method: method.to_string(),
suburl: suburl.to_string(),
body: body.to_vec(),
};
let resp = self
.runtime
.http(&req)
.map_err(|e| anyhow::anyhow!("{}: {e}", self.repo_name))?;
Ok((resp.status, resp.headers, resp.body))
}
fn coordinate_for_path(&self, suburl: &str) -> anyhow::Result<Option<ArtifactId>> {
Ok(self.runtime.coordinate_for_path(suburl).map(id_from_wire))
}
}
#[cfg(test)]
mod drift_tests {
use super::*;
struct Lists(WireManifest);
impl HandlerRuntime for Lists {
fn manifest(&self) -> &WireManifest {
&self.0
}
fn fetch(&self, _: &WireArtifactId) -> Result<Option<Vec<u8>>, String> {
Ok(None)
}
fn put(&self, _: &WireArtifactId, _: &[u8]) -> Result<(), String> {
Err("ro".into())
}
fn list(&self, _: Option<&str>, _: usize) -> Result<Vec<WireArtifactEntry>, String> {
Ok(Vec::new())
}
fn coordinate_for_path(&self, _: &str) -> Option<WireArtifactId> {
None
}
fn http(&self, _: &WireHttpRequest) -> Result<WireHttpResponse, String> {
Err("none".into())
}
}
/// RED drift bug: `list` is overridden, so a counter must be told the
/// listing is real — the flag stayed at the trait default `false`.
#[test]
fn a_handler_that_lists_says_it_lists() {
let m = WireManifest {
abi_version: holger_plugin_abi::ABI_VERSION,
handler: "h".into(),
format: "raw".into(),
writable: false,
identity: None,
};
let b = HandlerBackend::new("r", Box::new(Lists(m))).unwrap();
assert!(
b.caps().listing,
"HandlerBackend overrides list() but did not claim the listing"
);
// The ABI makes `coordinate_for_path` mandatory: the inverse of the
// handler's own path scheme, so the mapping is total.
assert!(
b.caps().path_mapping,
"HandlerBackend overrides coordinate_for_path() but did not claim the mapping"
);
traits::backend::conformance::check(&b).unwrap();
}
}