use crate::backends::ExternalBackend;
use crate::channels::capture::CaptureService;
use crate::engine::events::TaskEvent;
use crate::engine::router::Router;
use crate::engine::runner::{TaskRunner, WeightSignal};
use crate::engine::tasks::TaskManager;
use crate::store::TaskStore;
use crate::tmux::TmuxManager;
use dashmap::DashMap;
use std::sync::Arc;
use tokio::sync::broadcast;
use tokio::sync::{mpsc, RwLock, Semaphore};
type SessionMap = std::collections::HashMap<String, bool>;
async fn prepare_dispatch_context<F>(
router_arc: &Arc<RwLock<Router>>,
session_map_fut: F,
) -> (crate::engine::tick::DispatchMode, SessionMap)
where
F: std::future::Future<Output = SessionMap>,
{
let dispatch_mode = {
let router_guard = router_arc.read().await;
crate::engine::tick::dispatch_mode_from_router(&router_guard)
};
let session_map = session_map_fut.await;
(dispatch_mode, session_map)
}
#[allow(clippy::too_many_arguments)]
pub fn spawn(
mut rx: broadcast::Receiver<TaskEvent>,
backend: Arc<dyn ExternalBackend>,
tmux: Arc<TmuxManager>,
runner: Arc<TaskRunner>,
capture: Arc<CaptureService>,
semaphore: Arc<Semaphore>,
task_manager: Arc<TaskManager>,
weight_tx: mpsc::Sender<WeightSignal>,
router_arc: Arc<RwLock<Router>>,
dispatching: Arc<DashMap<String, String>>,
store: Arc<TaskStore>,
repo: String,
) {
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(event) if event.new_status == "routed" && event.repo == repo => {
tracing::info!(task_id = event.task_id, "event-driven dispatch triggered");
let (dispatch_mode, session_map) =
prepare_dispatch_context(&router_arc, tmux.batch_session_active()).await;
if let Err(e) = crate::engine::tick::tick_dispatch_tasks(
&backend,
&tmux,
&repo,
&runner,
&capture,
&semaphore,
&task_manager,
&weight_tx,
dispatch_mode,
&dispatching,
&store,
&session_map,
)
.await
{
tracing::warn!(?e, "event-driven dispatch failed (tick will retry)");
}
}
Ok(_) => {} Err(broadcast::error::RecvError::Lagged(n)) => {
tracing::debug!(n, "dispatch subscriber lagged, tick will catch up");
}
Err(_) => break, }
}
});
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::router::{Router, RouterConfig};
use std::time::Duration;
use tokio::sync::{oneshot, Notify};
#[tokio::test]
async fn prepare_dispatch_context_drops_read_lock_before_awaiting_session_map() {
let router_arc = Arc::new(RwLock::new(Router::new(RouterConfig::default())));
let (tx, rx) = oneshot::channel::<SessionMap>();
let waiting = Arc::new(Notify::new());
let waiting_for_task = Arc::clone(&waiting);
let router_for_task = Arc::clone(&router_arc);
let task = tokio::spawn(async move {
prepare_dispatch_context(&router_for_task, async move {
waiting_for_task.notify_one();
rx.await.unwrap_or_default()
})
.await
});
waiting.notified().await;
let write_guard = tokio::time::timeout(Duration::from_secs(2), router_arc.write())
.await
.expect("router write lock blocked while dispatch subscriber awaited session map");
drop(write_guard);
let _ = tx.send(SessionMap::new());
let _ = task.await.expect("prepare_dispatch_context task panicked");
}
}