prvng_platform/crates/extension-registry/src/events.rs
Jesús Pérez 93b0e5225c
feat(platform): control plane — NATS JetStream + SurrealDB + SOLID enforcement
New crates
  - platform-nats: async_nats JetStream bridge; pull/push consumers, explicit ACK,
    subject prefixing under provisioning.>, 6 stream definitions on startup
  - platform-db: SurrealDB pool (embedded RocksDB solo, Surreal<Mem> tests,
    WebSocket server multi-user); migrate() with DEFINE TABLE IF NOT EXISTS DDL

  Service integrations
  - orchestrator: NATS pub on task state transitions, execution_logs → SurrealDB,
    webhook handler (HMAC-SHA256), AuditCollector (batch INSERT, 100-event/1s flush)
  - control-center: solo_auth_middleware (intentional bypass, --mode solo only),
    NATS session events, WebSocket bridge via JetStream subscription (no polling)
  - vault-service: NATS lease flow; credentials over HTTPS only (lease_id in NATS);
    SurrealDB storage backend with MVCC retry + exponential backoff
  - secretumvault: complete SurrealDB backend replacing HashMap; 9 unit + 19 integration tests
  - extension-registry: NATS lifecycle events, vault:// credential resolver with TTL cache,
    cache invalidation via provisioning.workspace.*.deploy.done

  Clippy workspace clean
  cargo clippy --workspace -- -D warnings: 0 errors
  Patterns fixed: derivable_impls (#[default] on enum variants), excessive_nesting
  (let-else, boolean arithmetic in retain, extracted helpers), io_error_other,
  redundant_closure, iter_kv_map, manual_range_contains, pathbuf_instead_of_path
2026-02-17 23:58:14 +00:00

105 lines
3.2 KiB
Rust

use std::sync::Arc;
use futures::StreamExt;
use platform_nats::NatsBridge;
use serde::Serialize;
use tracing::{error, info, warn};
use crate::cache::ExtensionCache;
use crate::models::ExtensionType;
/// Publishes NATS events for extension lifecycle operations.
pub struct EventPublisher {
bridge: Arc<NatsBridge>,
}
#[derive(Debug, Serialize)]
struct ExtensionEvent<'a> {
name: &'a str,
version: &'a str,
#[serde(rename = "type")]
extension_type: ExtensionType,
}
impl EventPublisher {
pub fn new(bridge: Arc<NatsBridge>) -> Self {
Self { bridge }
}
/// Publish `provisioning.extensions.{type}.installed`.
///
/// Fire-and-forget: logs error on failure but never propagates to handler.
pub async fn publish_installed(
&self,
extension_type: ExtensionType,
name: &str,
version: &str,
) {
let subject = format!("extensions.{}.installed", extension_type);
let payload = ExtensionEvent {
name,
version,
extension_type,
};
match self.bridge.publish_json(&subject, &payload).await {
Ok(_) => {
info!(
subject = %subject,
extension = %name,
version = %version,
"Extension installed event published"
);
}
Err(e) => {
error!(
subject = %subject,
extension = %name,
"Failed to publish extension event: {}", e
);
}
}
}
}
/// Spawn a background task that subscribes to workspace deploy-done events
/// and invalidates the extension cache on each notification.
///
/// Subject: `provisioning.workspace.*.deploy.done` (filter on WORKSPACE stream)
pub fn spawn_cache_invalidator(bridge: Arc<NatsBridge>, cache: Arc<ExtensionCache>) {
tokio::spawn(async move {
run_cache_invalidator(bridge, cache).await;
});
}
async fn run_cache_invalidator(bridge: Arc<NatsBridge>, cache: Arc<ExtensionCache>) {
const STREAM: &str = "WORKSPACE";
const CONSUMER: &str = "ext-registry-cache-invalidator";
let mut messages = match bridge.subscribe_pull(STREAM, CONSUMER).await {
Ok(m) => m,
Err(e) => {
warn!("Extension registry cache invalidator: subscribe failed — {e}");
return;
}
};
info!("Extension registry cache invalidator running on stream {STREAM}");
while let Some(msg_result) = messages.next().await {
match msg_result {
Ok(msg) => {
// Subject pattern: provisioning.workspace.{ws_id}.deploy.done
// Filter is applied at JetStream level; any message here triggers invalidation.
let subject = msg.subject.as_str();
info!(subject = %subject, "Workspace deploy detected — invalidating extension cache");
cache.invalidate_all();
if let Err(e) = msg.ack().await {
warn!("Cache invalidator: ack failed — {e}");
}
}
Err(e) => {
error!("Cache invalidator: message error — {e}");
}
}
}
}