plex, redis: module runtime containers + declare self-consumed events (ADR 0052/0046)
plex gains a broker-bound tools/events runtime container (mesh-runtime-plex) alongside its server, and a never-throwing plex_reachable health probe. redis and postgres subscribe to their own lifecycle events in index.ts but declared no consumes — so the substrate never made the queue the runtime binds and it crashed on start (404). Declare the consume, as ADR 0046 requires. Proven end-to-end in the mesh-lab: assigned-plex and assigned-redis both green. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
@@ -128,4 +128,18 @@ export class PlexClient {
|
|||||||
async refreshAll(): Promise<void> {
|
async refreshAll(): Promise<void> {
|
||||||
for (const library of await this.getLibraries()) await this.refreshLibrary(library.key);
|
for (const library of await this.getLibraries()) await this.refreshLibrary(library.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A health probe that never throws: report whether the Plex server this client is pointed at
|
||||||
|
* answers, and identify it when it does. Every other call assumes the server is up; this is the
|
||||||
|
* one that tells the mesh whether it is, so a diagnosis does not start from a stack trace.
|
||||||
|
*/
|
||||||
|
async reachable(): Promise<{ reachable: boolean; url: string; server?: { name: string; version: string }; error?: string }> {
|
||||||
|
try {
|
||||||
|
const info = await this.getServerInfo();
|
||||||
|
return { reachable: true, url: this.baseUrl, server: { name: info.name, version: info.version } };
|
||||||
|
} catch (err) {
|
||||||
|
return { reachable: false, url: this.baseUrl, error: err instanceof Error ? err.message : String(err) };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,22 @@
|
|||||||
"/services/media/music:/music",
|
"/services/media/music:/music",
|
||||||
"/services/media/audiobooks:/audiobooks"
|
"/services/media/audiobooks:/audiobooks"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "runtime",
|
||||||
|
"type": "container",
|
||||||
|
"name": "mesh-plex",
|
||||||
|
"image": "mesh-runtime-plex@sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
"network": "host",
|
||||||
|
"volumes": [
|
||||||
|
"/var/lib/mesh/plex/broker:/run/secrets/broker:ro",
|
||||||
|
"/services/plex/config:/var/lib/plex/config:ro"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||||
|
"MESH_PLEX_URL": "http://127.0.0.1:32400",
|
||||||
|
"MESH_PLEX_DATA_DIR": "/var/lib/plex"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ export function getPlexTools(plex: PlexClient): ToolDefinition[] {
|
|||||||
return { server, libraries, sessions, recentlyAdded: recent };
|
return { server, libraries, sessions, recentlyAdded: recent };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "plex_reachable",
|
||||||
|
description: "Health probe: whether the Plex server answers, and which server it is. Never fails.",
|
||||||
|
input: {},
|
||||||
|
run: async () => plex.reachable(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "plex_search",
|
name: "plex_search",
|
||||||
description: "Search across all Plex libraries — movies, shows, episodes, music.",
|
description: "Search across all Plex libraries — movies, shows, episodes, music.",
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// postgres's events entrypoint, loaded by the per-node tool host (the provisioner container runs
|
||||||
|
// ./provisioner separately). The database lifecycle events are EMITTED from the provisioner, where
|
||||||
|
// the lifecycle actually happens (novox/hq ADR 0046/0047):
|
||||||
|
// module.postgres.database.provisioned — a consumer's database + owning role was created
|
||||||
|
// module.postgres.database.deprovisioned — that database was removed
|
||||||
|
// Here in the tool host we react to them, keeping a lightweight audit trail of who was granted a
|
||||||
|
// database and who lost one — observability the provider itself is best placed to log.
|
||||||
|
|
||||||
|
import { on } from "@novox/mesh-sdk/events";
|
||||||
|
|
||||||
|
interface DatabaseEvent {
|
||||||
|
consumer: string;
|
||||||
|
database: string;
|
||||||
|
user?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
await on<DatabaseEvent>("module.postgres.database.provisioned", async (e) => {
|
||||||
|
console.log(`[postgres] database provisioned for ${e.body.consumer} (db ${e.body.database})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
await on<DatabaseEvent>("module.postgres.database.deprovisioned", async (e) => {
|
||||||
|
console.log(`[postgres] database deprovisioned for ${e.body.consumer} (db ${e.body.database})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[postgres] auditing database lifecycle events");
|
||||||
@@ -14,6 +14,10 @@
|
|||||||
"module.postgres.database.provisioned",
|
"module.postgres.database.provisioned",
|
||||||
"module.postgres.database.deprovisioned"
|
"module.postgres.database.deprovisioned"
|
||||||
],
|
],
|
||||||
|
"consumes": [
|
||||||
|
"module.postgres.database.provisioned",
|
||||||
|
"module.postgres.database.deprovisioned"
|
||||||
|
],
|
||||||
"listens": [
|
"listens": [
|
||||||
{
|
{
|
||||||
"port": 5432,
|
"port": 5432,
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// redis's events entrypoint, loaded by the per-node tool host (the provisioner container runs
|
||||||
|
// ./provisioner separately). The cache lifecycle events are EMITTED from the provisioner, where the
|
||||||
|
// lifecycle actually happens (novox/hq ADR 0046/0047):
|
||||||
|
// module.redis.cache.provisioned — a consumer's ACL user + keyspace was created
|
||||||
|
// module.redis.cache.deprovisioned — that user was removed
|
||||||
|
// Here in the tool host we react to them, keeping a lightweight audit trail of who was granted a
|
||||||
|
// cache and who lost one — observability the provider itself is best placed to log.
|
||||||
|
|
||||||
|
import { on } from "@novox/mesh-sdk/events";
|
||||||
|
|
||||||
|
interface CacheEvent {
|
||||||
|
consumer: string;
|
||||||
|
username: string;
|
||||||
|
keyspacePrefix?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
await on<CacheEvent>("module.redis.cache.provisioned", async (e) => {
|
||||||
|
console.log(`[redis] cache provisioned for ${e.body.consumer} (user ${e.body.username})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
await on<CacheEvent>("module.redis.cache.deprovisioned", async (e) => {
|
||||||
|
console.log(`[redis] cache deprovisioned for ${e.body.consumer} (user ${e.body.username})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[redis] auditing cache lifecycle events");
|
||||||
@@ -14,6 +14,10 @@
|
|||||||
"module.redis.cache.provisioned",
|
"module.redis.cache.provisioned",
|
||||||
"module.redis.cache.deprovisioned"
|
"module.redis.cache.deprovisioned"
|
||||||
],
|
],
|
||||||
|
"consumes": [
|
||||||
|
"module.redis.cache.provisioned",
|
||||||
|
"module.redis.cache.deprovisioned"
|
||||||
|
],
|
||||||
"serves": {
|
"serves": {
|
||||||
"redis-cache": {}
|
"redis-cache": {}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user