use std::sync::Arc;
use axum::extract::FromRef;
use sqlx::PgPool;
use tokio::sync::Semaphore;
use tokio_util::task::TaskTracker;
use uuid::Uuid;
use yorishiro_core::db::TenantDb;
use yorishiro_core::models::entities::EntityRecord;
use yorishiro_core::services::auth::{Authenticator, default_authenticator};
use yorishiro_core::services::embedding::EmbeddingProvider;
use yorishiro_core::services::embedding::sync as embedding_sync;
use yorishiro_core::services::queue::{LocalQueue, Queue};
use yorishiro_core::{ResultExt, YorishiroError};
const EMBEDDING_SYNC_MAX_CONCURRENCY: usize = 4;
const EMBEDDING_SYNC_MAX_RETRIES: u32 = 3;
#[derive(Clone)]
pub struct AppState {
pub tenant_db: TenantDb,
pub identity_pool: PgPool,
pub embedding_provider: Arc<dyn EmbeddingProvider>,
pub search_token_limiter: Arc<crate::http::middleware::rate_limit::RateLimiter>,
pub authenticator: Arc<dyn Authenticator>,
embedding_sync_permits: Arc<Semaphore>,
embedding_tasks: TaskTracker,
queue: Arc<dyn Queue>,
}
impl AppState {
pub fn new(
tenant_db: TenantDb,
identity_pool: PgPool,
embedding_provider: Arc<dyn EmbeddingProvider>,
) -> Self {
Self {
tenant_db,
identity_pool,
embedding_provider,
search_token_limiter: Arc::new(
crate::http::middleware::rate_limit::RateLimiter::search_tokens_from_env(),
),
authenticator: default_authenticator(),
embedding_sync_permits: Arc::new(Semaphore::new(EMBEDDING_SYNC_MAX_CONCURRENCY)),
embedding_tasks: TaskTracker::new(),
queue: Arc::new(LocalQueue::new(EMBEDDING_SYNC_MAX_CONCURRENCY)),
}
}
pub fn charge_search_tokens(
&self,
workspace_id: uuid::Uuid,
query_text: &str,
) -> Result<(), YorishiroError> {
let tokens = self.embedding_provider.count_tokens(query_text);
if self
.search_token_limiter
.allow_cost(&workspace_id.to_string(), tokens)
{
return Ok(());
}
tracing::warn!(%workspace_id, tokens, "search token budget exhausted");
Err(YorishiroError::ValidationFailed {
message: "this workspace has spent its search token budget for the minute".to_string(),
details: vec![],
hint: "retry shortly, or raise YORISHIRO_SEARCH_TOKENS_PER_MINUTE".to_string(),
})
}
pub fn with_authenticator(mut self, authenticator: Arc<dyn Authenticator>) -> Self {
self.authenticator = authenticator;
self
}
pub fn embedding_tasks(&self) -> &TaskTracker {
&self.embedding_tasks
}
pub fn enqueue(&self, task: yorishiro_core::services::queue::Task) {
self.queue.enqueue(task);
}
pub async fn drain_queue(&self, timeout: std::time::Duration) {
self.queue.drain(timeout).await;
}
pub fn spawn_embedding_sync(
&self,
tenant_id: Uuid,
workspace_id: Uuid,
record: EntityRecord,
) -> tokio::task::JoinHandle<()> {
let db = self.tenant_db.clone();
let provider = Arc::clone(&self.embedding_provider);
let permits = Arc::clone(&self.embedding_sync_permits);
self.embedding_tasks.spawn(async move {
let Ok(_permit) = permits.acquire_owned().await else {
return;
};
let mut attempt = 0;
let result = loop {
let outcome = async {
let mut conn = db
.acquire_for_workspace(tenant_id, workspace_id)
.await
.internal()?;
embedding_sync::sync_embedding_for_record(
&mut conn,
workspace_id,
&record,
provider.as_ref(),
)
.await
}
.await;
match outcome {
Err(YorishiroError::ProviderBusy {
ref message,
retry_after,
}) if attempt < EMBEDDING_SYNC_MAX_RETRIES => {
attempt += 1;
tracing::info!(
entity_id = %record.id,
attempt,
retry_after_secs = retry_after.as_secs(),
%message,
"embedding provider busy; waiting before retry"
);
tokio::time::sleep(retry_after).await;
}
other => break other,
}
};
if let Err(err) = result {
tracing::warn!(entity_id = %record.id, error = %err, "embedding sync failed");
}
})
}
}
impl FromRef<AppState> for TenantDb {
fn from_ref(state: &AppState) -> Self {
state.tenant_db.clone()
}
}
#[cfg(test)]
#[path = "../tests/state.rs"]
mod tests;