106 lines
3.2 KiB
Rust
106 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}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|