use std::sync::Arc;
use tokio::sync::Semaphore;
use tokio::task::JoinSet;
use crate::meta_storage::{
MetaBackend, MetaClientApi, MetaConnApi, MetaStorageError, MetaTicketApi, MetaTxApi,
};
use crate::scheduler::events::{
IndividualControlEvent, IndividualControlEventReceiver, InternalEvent, PeerEvent,
PeerEventSenders, ServicePeerEventReceiver, ServicePeerEventSenderMap,
};
use crate::scheduler::queue::{AnyJobQueue, JobQueue};
use crate::scheduler::{SchedulerError, SpecWithMetadata, TaskSpec};
use crate::schema::{Job, SharedProgress, TaskMetadata, TaskState, Ticket, TicketStatus};
use crate::service::OperonService;
use crate::storage::OperonStorage;
type WorkerResult<J, R, UErr, SErr, MErr> =
Result<InternalEvent<J, R, UErr, SErr, MErr>, SchedulerError<UErr, SErr, MErr>>;
type WorkerHandles<J, R, UErr, SErr, MErr> = JoinSet<WorkerResult<J, R, UErr, SErr, MErr>>;
pub(super) struct IndividualScheduler<Svc, Sto, TS, MSto, const N: usize>
where
Svc: OperonService,
Sto: OperonStorage,
MSto: MetaBackend,
TS: TaskSpec<Svc, Sto, MSto>,
{
pub spec: TS,
pub meta: TaskMetadata<N>,
pub service: Arc<Svc>,
pub storage: Arc<Sto>,
pub meta_storage: MSto,
pub pool: Arc<Semaphore>,
pub progress: SharedProgress,
pub state: TaskState,
pub handles: WorkerHandles<Job<N>, TS::Resolution, Svc::Error, Sto::Error, MSto::Error>,
}
impl<Svc, Sto, TS, MSto, const N: usize> IndividualScheduler<Svc, Sto, TS, MSto, N>
where
Svc: OperonService,
Sto: OperonStorage,
MSto: MetaBackend,
TS: TaskSpec<Svc, Sto, MSto, Job = Job<N>, Ticket = Ticket<N>>,
{
pub(super) fn new(
spec: SpecWithMetadata<Svc, Sto, TS, N>,
service: Arc<Svc>,
storage: Arc<Sto>,
meta_storage: MSto,
pool_size: usize,
progress: SharedProgress,
) -> Self {
Self {
spec: spec.spec,
meta: spec.task_meta,
storage,
service,
meta_storage,
pool: Arc::new(Semaphore::new(pool_size)),
progress,
state: TaskState::Running,
handles: JoinSet::new(),
}
}
async fn set_state(&mut self, state: TaskState) {
self.state = state;
(*self.progress.write().await).set_state(state);
}
async fn update_progress(
&mut self,
) -> Result<(), SchedulerError<Svc::Error, Sto::Error, MSto::Error>> {
let conn = self.meta_storage.scheduler_conn().await?;
self.update_progress_with_client(conn.as_client()).await?;
Ok(())
}
async fn update_progress_with_client(
&mut self,
client: MSto::Client<'_>,
) -> Result<(), SchedulerError<Svc::Error, Sto::Error, MSto::Error>> {
let (done, queued, waiting) = client.ticket(self.meta).get_status().await?;
let finished = (*self.progress.write().await).update(done, queued, waiting);
if finished && self.state != TaskState::Finished {
self.state = TaskState::Finished;
tracing::info!("All `{}` jobs are finished.", self.meta.id);
}
Ok(())
}
async fn initial_ready_tickets(&self) -> Result<Vec<Ticket<N>>, MetaStorageError<MSto::Error>> {
let conn = self.meta_storage.scheduler_conn().await?;
conn.as_client()
.ticket(self.meta)
.get_all(TicketStatus::Queued)
.await
}
async fn on_event_ready_jobs(
&mut self,
event: PeerEvent<Svc::JobEnum, Svc::ResolutionEnum, Svc::TicketEnum>,
peer_txs: &TS::PeerEventSenders,
) -> Result<Vec<Job<N>>, SchedulerError<Svc::Error, Sto::Error, MSto::Error>> {
let mut conn = self.meta_storage.scheduler_conn().await?;
let tx = conn.transaction().await?;
let ready_tickets = match event {
PeerEvent::Job(job) => self.spec.on_receive_job(tx.as_client(), job).await?,
PeerEvent::Resolution(resolution) => {
self.spec
.on_receive_resolution(tx.as_client(), peer_txs, resolution)
.await?
}
PeerEvent::Explosion(explosion) => {
self.spec
.on_receive_explosion(tx.as_client(), explosion)
.await?
}
};
self.update_progress_with_client(tx.as_client()).await?;
tx.commit().await?;
let ready_jobs = ready_tickets
.iter()
.map(|t| t.resolve())
.collect::<Option<Vec<_>>>()
.ok_or(SchedulerError::other(
"Some ready tickets are not actually ready to run",
))?;
Ok(ready_jobs)
}
fn check_initial_data(&self, tickets: &[Ticket<N>]) -> bool {
let mut all_ready = true;
for ticket in tickets.iter().filter(|t| !t.is_ready()) {
tracing::error!(
"Restored ticket for `{}` job is not ready to run: {:?}",
self.meta.id,
ticket
);
all_ready = false;
}
all_ready
}
pub(super) async fn run(
mut self,
peer_tx_map: ServicePeerEventSenderMap<Svc>,
peer_rx: ServicePeerEventReceiver<Svc>,
ctrl_rx: IndividualControlEventReceiver,
clean: bool,
) {
let peer_txs = TS::PeerEventSenders::gather_from(peer_tx_map);
let Ok(initial_tickets) = self.initial_ready_tickets().await else {
self.set_state(TaskState::Error).await;
return;
};
if !clean && !self.check_initial_data(&initial_tickets) {
self.set_state(TaskState::Error).await;
return;
}
if let Err(e) = self.update_progress().await {
tracing::error!("Failed to update UI state after initial data processing: {e}");
self.set_state(TaskState::Error).await;
return;
}
if self.state == TaskState::Finished {
tracing::debug!(
"Scheduler for `{}` exited due to being finished from the start.",
self.meta.id
);
if let Err(e) = self.update_progress().await {
tracing::error!("Failed to update UI state after scheduler run: {e}");
self.set_state(TaskState::Error).await;
}
return;
}
let res = self
.run_internal(initial_tickets, &peer_txs, peer_rx, ctrl_rx)
.await;
let id = &self.meta.id;
if let Err(e) = &res {
tracing::error!("Scheduler for `{id}` exited with an error: {e}");
self.set_state(TaskState::Error).await;
} else {
match self.state {
TaskState::Finished => tracing::debug!("Scheduler for `{id}` exited normally."),
TaskState::Stopped => tracing::debug!("Scheduler for `{id}` was stopped."),
_ => {
tracing::error!(
"Scheduler for `{id}` exited with an unexpected state: {:?}",
self.state,
);
self.set_state(TaskState::Error).await;
}
}
}
if let Err(e) = self.update_progress().await {
tracing::error!("Failed to update UI state after scheduler run: {e}");
self.set_state(TaskState::Error).await;
}
}
async fn run_internal(
&mut self,
initial_tickets: Vec<Ticket<N>>,
peer_txs: &TS::PeerEventSenders,
mut peer_rx: ServicePeerEventReceiver<Svc>,
mut ctrl_rx: IndividualControlEventReceiver,
) -> Result<(), SchedulerError<Svc::Error, Sto::Error, MSto::Error>> {
let pool = self.pool.clone();
let initial_jobs = initial_tickets
.iter()
.map(|t| t.resolve())
.collect::<Option<Vec<_>>>()
.ok_or(SchedulerError::other(
"Some initial tickets are not actually ready to run",
))?;
let mut ready_jobs = AnyJobQueue::from_meta(initial_jobs, &self.meta);
let mut got_all_peer_events = false;
let mut is_gracefully_stopping = false;
loop {
if self.state == TaskState::Finished && self.handles.is_empty() {
return Ok(());
}
if is_gracefully_stopping && self.handles.is_empty() && got_all_peer_events {
self.set_state(TaskState::Stopped).await;
return Ok(());
}
::tokio::select! {
Some(ctrl_event) = ctrl_rx.recv() => {
match ctrl_event {
IndividualControlEvent::Pause if self.state == TaskState::Running => self.handle_pause().await,
IndividualControlEvent::Resume if self.state == TaskState::Paused => self.handle_resume().await,
IndividualControlEvent::Quit { force: false } => {
self.handle_graceful_stop().await;
is_gracefully_stopping = true;
}
IndividualControlEvent::Quit { force: true } => {
tracing::info!("Aborting `{}` jobs.", self.meta.id);
self.handles.abort_all();
if matches!(self.state, TaskState::Running | TaskState::Paused) {
self.set_state(TaskState::Stopped).await;
}
return Ok(());
}
_ => {}
}
}
Some(int_event) = self.handles.join_next() => {
let int_event = int_event??;
self.update_progress().await?;
match int_event {
InternalEvent::JobSuccess(job, resolution) => {
tracing::trace!(
"{} received internal event: JobSuccess({job:?}, {resolution:?}).",
self.meta.id
);
self.spec.send_on_finish(peer_txs, job, resolution).await?;
}
InternalEvent::JobFailure(job, e) => {
tracing::error!("Job {job:?} failed: {e}");
return Err(e);
}
}
}
event = peer_rx.recv(), if !got_all_peer_events => {
match event {
Some(evt) => {
tracing::trace!(
"{} received peer event: {evt:?}; \
Peer channel has {} events left.",
self.meta.id, peer_rx.len()
);
ready_jobs.extend(self.on_event_ready_jobs(evt, peer_txs).await?)
},
None => {
tracing::debug!("`{}` finished receiving updates.", self.meta.id);
got_all_peer_events = true;
}
}
}
permit = pool.clone().acquire_owned(),
if !ready_jobs.is_empty() && self.state == TaskState::Running
=> {
let permit = permit?;
let job = ready_jobs.pop().ok_or(SchedulerError::other("Ready to run queue is empty"))?;
let task_id = self.meta.id;
let spec = self.spec.clone();
let storage = self.storage.clone();
let service = self.service.clone();
let meta_storage = self.meta_storage.clone();
let _abort_handle = self.handles.spawn(async move {
tracing::trace!("Running job {job:?} in `{task_id}` scheduler.");
let _permit = permit;
match spec.run_job(&*service, &*storage, meta_storage, job).await {
Ok(resolution) => {
tracing::trace!(
"{task_id} worker exited with: JobSuccess({job:?}, {resolution:?}).",
);
Ok(InternalEvent::JobSuccess(job, resolution))
}
Err(e) => {
tracing::trace!(
"{task_id} worker exited with: JobFailure({job:?}, {e:?});",
);
Ok(InternalEvent::JobFailure(job, e))
}
}
});
}
}
}
}
async fn handle_pause(&mut self) {
tracing::info!("Pausing `{}` jobs.", self.meta.id);
self.set_state(TaskState::Paused).await;
}
async fn handle_resume(&mut self) {
tracing::info!("Resuming `{}` jobs.", self.meta.id);
self.set_state(TaskState::Running).await;
}
async fn handle_graceful_stop(&mut self) {
if self.state == TaskState::Paused {
return;
}
tracing::info!("Pausing `{}` jobs for graceful stop.", self.meta.id);
self.set_state(TaskState::Paused).await;
}
}