Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c34d4a8ab2 | ||
|
|
815d902dda |
@@ -1 +1,70 @@
|
|||||||
# meta-feeder-book
|
# meta-feeder-book
|
||||||
|
|
||||||
|
Book **feeder sidecar** for [MetaMesh](https://github.com/worph/meta-gateway) —
|
||||||
|
surfaces public-domain books from **Project Gutenberg** (via the
|
||||||
|
[Gutendex](https://gutendex.com) API) into a meta-gateway.
|
||||||
|
|
||||||
|
It is the proof-of-concept feeder: a single upstream, no preview-seeding, no
|
||||||
|
BitTorrent — just enough to exercise the `meta-feeder-sdk` + feeder HTTP
|
||||||
|
contract end to end.
|
||||||
|
|
||||||
|
## Role in MetaMesh
|
||||||
|
|
||||||
|
A feeder is a stateless HTTP sidecar. It **finds records and fetches bytes**; it
|
||||||
|
does *not* talk to meta-core or the libp2p blockstore. The gateway core that
|
||||||
|
calls it owns the meta-core store-back and the blockstore seeding. A gateway
|
||||||
|
registers this feeder as a `RemoteFeederPlugin` pointing at its `/` and then
|
||||||
|
drives the contract:
|
||||||
|
|
||||||
|
| Endpoint | Purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| `GET /manifest` | feeder identity + capabilities |
|
||||||
|
| `GET /health` | liveness |
|
||||||
|
| `POST /query`, `POST /query_stream` | structured search against the upstream |
|
||||||
|
| `POST /compute` | enrichment / outcome compute |
|
||||||
|
| `GET /fetch/:upstream_id/:record_id` | fetch a record's bytes |
|
||||||
|
| `GET /blob/:upstream_id/:cid` | fetch a content-addressed blob |
|
||||||
|
| `GET /config`, `GET /config/schema`, `GET\|PUT /config/values` | runtime config UI + API |
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Env var | Default | Notes |
|
||||||
|
|---------|---------|-------|
|
||||||
|
| `META_FEEDER_HTTP_LISTEN` | `0.0.0.0:8080` | HTTP listen address |
|
||||||
|
| `META_FEEDER_STATE_DIR` | `/data/meta-feeder` | redb cache + state |
|
||||||
|
| `RUST_LOG` | `info` | tracing filter |
|
||||||
|
|
||||||
|
No upstream API key is required — Gutendex is open.
|
||||||
|
|
||||||
|
## Image
|
||||||
|
|
||||||
|
```
|
||||||
|
ghcr.io/worph/meta-feeder-book
|
||||||
|
```
|
||||||
|
|
||||||
|
Exposes `8080`. Built and pushed by CI on every push to `main` (the `main`
|
||||||
|
tag) and on `v*` tags (semver tags).
|
||||||
|
|
||||||
|
## Build locally
|
||||||
|
|
||||||
|
The build context is the **repo root** (the Cargo workspace) so the vendored
|
||||||
|
`meta-feeder-sdk` path dependency resolves:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -f feeder-plugin/book-feeder/Dockerfile -t ghcr.io/worph/meta-feeder-book:dev .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Repo layout
|
||||||
|
|
||||||
|
This repo is a self-contained Cargo workspace vendored out of the
|
||||||
|
`meta-gateway` monorepo:
|
||||||
|
|
||||||
|
```
|
||||||
|
Cargo.toml # workspace: members = crates/*, feeder-plugin/*
|
||||||
|
crates/meta-feeder-sdk/ # vendored shared feeder SDK
|
||||||
|
feeder-plugin/book-feeder/ # this feeder's crate + Dockerfile
|
||||||
|
```
|
||||||
|
|
||||||
|
Upstream source of truth for the SDK and the feeder crate is
|
||||||
|
[`worph/meta-gateway`](https://github.com/worph/meta-gateway); changes there are
|
||||||
|
vendored back into this repo.
|
||||||
|
|||||||
@@ -478,10 +478,13 @@ pub fn router(
|
|||||||
/// Configure each plugin (cache dir under `state_dir/gateway/<id>`), then serve
|
/// Configure each plugin (cache dir under `state_dir/gateway/<id>`), then serve
|
||||||
/// the feeder HTTP contract on `listen` until shutdown.
|
/// the feeder HTTP contract on `listen` until shutdown.
|
||||||
///
|
///
|
||||||
/// A plugin whose `configure()` returns [`ConfigError::MissingConfig`] is
|
/// The feeder process never aborts on a config problem: a plugin whose
|
||||||
/// **soft-skipped** (warn + dropped) — mirroring the gateway registry's
|
/// `configure()` returns [`ConfigError::MissingConfig`] is **soft-skipped**
|
||||||
/// behaviour, so a misconfigured plugin doesn't take the whole feeder down.
|
/// (warn + dropped — an optional source the operator didn't ask for), and any
|
||||||
/// Any other `ConfigError` aborts startup.
|
/// other failure (bad/empty key, cache-dir setup, …) registers a
|
||||||
|
/// [`DegradedPlugin`] that stays up and reports the reason via `/health` and
|
||||||
|
/// `/manifest`. The container keeps running so the error is visible in the UX
|
||||||
|
/// instead of crash-looping.
|
||||||
pub async fn serve_feeders(
|
pub async fn serve_feeders(
|
||||||
plugins: Vec<Box<dyn FeederPlugin>>,
|
plugins: Vec<Box<dyn FeederPlugin>>,
|
||||||
state_dir: impl Into<PathBuf>,
|
state_dir: impl Into<PathBuf>,
|
||||||
@@ -503,9 +506,64 @@ pub async fn serve_feeders(
|
|||||||
.map_err(anyhow::Error::from)
|
.map_err(anyhow::Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run each plugin's `configure()` against its per-plugin cache dir, applying
|
/// A plugin whose `configure()` (or cache-dir setup) failed. It stays
|
||||||
/// the soft-skip rule, and return the surviving plugins keyed by `upstream_id`.
|
/// **registered** so the failure is visible at `GET /health` (as
|
||||||
/// Public so feeder binaries / tests can configure without serving.
|
/// [`PluginHealth::Degraded`]) and `GET /manifest`; any query/compute against it
|
||||||
|
/// returns the captured reason as a permanent error. This is what keeps one
|
||||||
|
/// misconfigured feeder from crash-looping the whole container — the operator
|
||||||
|
/// sees the reason in the UX and fixes the config, no restart-loop required.
|
||||||
|
struct DegradedPlugin {
|
||||||
|
id: &'static str,
|
||||||
|
reason: String,
|
||||||
|
file_types: &'static [&'static str],
|
||||||
|
content_kinds: &'static [&'static str],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl FeederPlugin for DegradedPlugin {
|
||||||
|
fn upstream_id(&self) -> &'static str {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure(&mut self, _cache_dir: &FsPath) -> Result<(), ConfigError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_query(
|
||||||
|
&self,
|
||||||
|
_query: &GatewayQuery,
|
||||||
|
_max_results: usize,
|
||||||
|
) -> Result<Vec<DiscoveryRecord>, GatewayError> {
|
||||||
|
Err(GatewayError::Permanent(self.reason.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn compute_outcomes(
|
||||||
|
&self,
|
||||||
|
_record_id: &str,
|
||||||
|
) -> Result<Vec<HashOutcome>, GatewayError> {
|
||||||
|
Err(GatewayError::Permanent(self.reason.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn health(&self) -> PluginHealth {
|
||||||
|
PluginHealth::Degraded {
|
||||||
|
reason: self.reason.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn served_file_types(&self) -> &'static [&'static str] {
|
||||||
|
self.file_types
|
||||||
|
}
|
||||||
|
|
||||||
|
fn served_content_kinds(&self) -> &'static [&'static str] {
|
||||||
|
self.content_kinds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run each plugin's `configure()` against its per-plugin cache dir and return
|
||||||
|
/// the plugins keyed by `upstream_id`. Never errors on a per-plugin problem:
|
||||||
|
/// `MissingConfig` is soft-skipped, every other failure becomes a
|
||||||
|
/// [`DegradedPlugin`] (see [`serve_feeders`]). Public so feeder binaries /
|
||||||
|
/// tests can configure without serving.
|
||||||
pub fn configure_plugins(
|
pub fn configure_plugins(
|
||||||
plugins: Vec<Box<dyn FeederPlugin>>,
|
plugins: Vec<Box<dyn FeederPlugin>>,
|
||||||
state_dir: &FsPath,
|
state_dir: &FsPath,
|
||||||
@@ -513,14 +571,39 @@ pub fn configure_plugins(
|
|||||||
let mut out: HashMap<String, Arc<dyn FeederPlugin>> = HashMap::new();
|
let mut out: HashMap<String, Arc<dyn FeederPlugin>> = HashMap::new();
|
||||||
for mut plugin in plugins {
|
for mut plugin in plugins {
|
||||||
let id = plugin.upstream_id();
|
let id = plugin.upstream_id();
|
||||||
|
// Captured up front (both are `'static`, so this borrow of `plugin`
|
||||||
|
// ends immediately) — lets us build a DegradedPlugin without holding a
|
||||||
|
// borrow across `configure(&mut self)` / the move into `out`.
|
||||||
|
let file_types = plugin.served_file_types();
|
||||||
|
let content_kinds = plugin.served_content_kinds();
|
||||||
let cache_dir = state_dir.join("gateway").join(id);
|
let cache_dir = state_dir.join("gateway").join(id);
|
||||||
std::fs::create_dir_all(&cache_dir).map_err(|e| {
|
|
||||||
anyhow::anyhow!("create cache dir {} for {id}: {e}", cache_dir.display())
|
// Build (don't insert) a degraded stand-in: keeps the container up and
|
||||||
})?;
|
// surfaces the reason via /health instead of crash-looping.
|
||||||
|
let make_degraded = |reason: String| -> Arc<dyn FeederPlugin> {
|
||||||
|
warn!(
|
||||||
|
target: "meta-feeder",
|
||||||
|
upstream_id = id,
|
||||||
|
%reason,
|
||||||
|
"feeder plugin degraded: container stays up; error surfaced via /health"
|
||||||
|
);
|
||||||
|
Arc::new(DegradedPlugin {
|
||||||
|
id,
|
||||||
|
reason,
|
||||||
|
file_types,
|
||||||
|
content_kinds,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = std::fs::create_dir_all(&cache_dir) {
|
||||||
|
let reason = format!("cache dir {} setup failed: {e}", cache_dir.display());
|
||||||
|
out.insert(id.to_string(), make_degraded(reason));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
match plugin.configure(&cache_dir) {
|
match plugin.configure(&cache_dir) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let id = id.to_string();
|
out.insert(id.to_string(), Arc::from(plugin));
|
||||||
out.insert(id, Arc::from(plugin));
|
|
||||||
}
|
}
|
||||||
Err(ConfigError::MissingConfig { plugin: p, what }) => {
|
Err(ConfigError::MissingConfig { plugin: p, what }) => {
|
||||||
warn!(
|
warn!(
|
||||||
@@ -531,7 +614,9 @@ pub fn configure_plugins(
|
|||||||
other plugins continue"
|
other plugins continue"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(other) => return Err(anyhow::anyhow!(other)),
|
Err(other) => {
|
||||||
|
out.insert(id.to_string(), make_degraded(other.to_string()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
|
|||||||
Reference in New Issue
Block a user