наблюдаемость: ввести безопасный контракт метрик
CI / Rust Checks (pull_request) Successful in 6m15s
CI / UI Checks (pull_request) Successful in 5s
CI / Community Image Smoke (pull_request) Successful in 4m25s
CI / Frontend E2E (pull_request) Successful in 5m17s
CI / Deploy (pull_request) Has been skipped
CI / Rust Checks (push) Successful in 6m9s
CI / UI Checks (push) Successful in 5s
CI / Community Image Smoke (push) Successful in 1m3s
CI / Frontend E2E (push) Successful in 3m47s
CI / Deploy (push) Failing after 3s
CI / Rust Checks (pull_request) Successful in 6m15s
CI / UI Checks (pull_request) Successful in 5s
CI / Community Image Smoke (pull_request) Successful in 4m25s
CI / Frontend E2E (pull_request) Successful in 5m17s
CI / Deploy (pull_request) Has been skipped
CI / Rust Checks (push) Successful in 6m9s
CI / UI Checks (push) Successful in 5s
CI / Community Image Smoke (push) Successful in 1m3s
CI / Frontend E2E (push) Successful in 3m47s
CI / Deploy (push) Failing after 3s
This commit is contained in:
@@ -9,9 +9,12 @@ use sqlx::{
|
||||
};
|
||||
use thiserror::Error;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::sync::{RwLock, mpsc};
|
||||
use tracing::{info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
const ACTIVE_SESSION_COUNT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(2);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SessionState {
|
||||
pub id: String,
|
||||
@@ -54,10 +57,72 @@ pub trait TransportSessionStore: Send + Sync {
|
||||
async fn delete(&self, session_id: &str) -> Result<bool, SessionStoreError>;
|
||||
|
||||
async fn cleanup_expired(&self, now: OffsetDateTime) -> Result<u64, SessionStoreError>;
|
||||
|
||||
async fn active_count(&self, now: OffsetDateTime) -> Result<u64, SessionStoreError>;
|
||||
}
|
||||
|
||||
pub type SharedSessionStore = Arc<dyn TransportSessionStore>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ActiveSessionMetrics {
|
||||
refresh_tx: mpsc::Sender<()>,
|
||||
}
|
||||
|
||||
impl ActiveSessionMetrics {
|
||||
pub fn start(sessions: SharedSessionStore) -> Self {
|
||||
let (refresh_tx, mut refresh_rx) = mpsc::channel(1);
|
||||
tokio::spawn(async move {
|
||||
while refresh_rx.recv().await.is_some() {
|
||||
match tokio::time::timeout(
|
||||
ACTIVE_SESSION_COUNT_TIMEOUT,
|
||||
sessions.active_count(OffsetDateTime::now_utc()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(count)) => crank_metrics::set_mcp_active_sessions(count),
|
||||
Ok(Err(_)) | Err(_) => {
|
||||
warn!(
|
||||
name: "mcp.active_session_metrics.refresh_failed",
|
||||
error_category = "session_store",
|
||||
"active session metrics refresh failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let metrics = Self { refresh_tx };
|
||||
metrics.refresh();
|
||||
metrics
|
||||
}
|
||||
|
||||
pub fn refresh(&self) {
|
||||
let _ = self.refresh_tx.try_send(());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_session_cleanup(
|
||||
sessions: SharedSessionStore,
|
||||
metrics: ActiveSessionMetrics,
|
||||
cleanup_interval: std::time::Duration,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(cleanup_interval);
|
||||
loop {
|
||||
interval.tick().await;
|
||||
match sessions.cleanup_expired(OffsetDateTime::now_utc()).await {
|
||||
Ok(removed) if removed > 0 => {
|
||||
info!(name: "mcp.session_cleanup.completed", removed);
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
warn!(name: "mcp.session_cleanup.failed", error_category = "session_store");
|
||||
}
|
||||
}
|
||||
metrics.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PostgresTransportSessionStore {
|
||||
pool: PgPool,
|
||||
@@ -176,6 +241,17 @@ impl TransportSessionStore for InMemorySessionStore {
|
||||
guard.retain(|_, session| !is_expired(session, now));
|
||||
Ok(u64::try_from(before.saturating_sub(guard.len())).unwrap_or(u64::MAX))
|
||||
}
|
||||
|
||||
async fn active_count(&self, now: OffsetDateTime) -> Result<u64, SessionStoreError> {
|
||||
let guard = self.inner.read().await;
|
||||
Ok(u64::try_from(
|
||||
guard
|
||||
.values()
|
||||
.filter(|session| !is_expired(session, now))
|
||||
.count(),
|
||||
)
|
||||
.unwrap_or(u64::MAX))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -319,6 +395,22 @@ impl TransportSessionStore for PostgresTransportSessionStore {
|
||||
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
async fn active_count(&self, now: OffsetDateTime) -> Result<u64, SessionStoreError> {
|
||||
let row = query(
|
||||
"select count(*)::bigint as active_count
|
||||
from mcp_transport_sessions
|
||||
where expires_at is null or expires_at > $1::timestamptz",
|
||||
)
|
||||
.bind(now)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(|error| SessionStoreError {
|
||||
details: error.to_string(),
|
||||
})?;
|
||||
let count = row.get::<i64, _>("active_count");
|
||||
Ok(u64::try_from(count).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
async fn apply_postgres_migrations(pool: &PgPool) -> Result<(), SessionStoreError> {
|
||||
|
||||
Reference in New Issue
Block a user