use crate::{
derive_idempotency_key, EventCursor, SubmissionEventPage, SubmissionResource, TaskOperation, TaskSubmissionRequest,
};
use std::{collections::HashMap, error::Error, future::Future, sync::Arc, time::Duration};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TaskChainPolicy {
pub chain_id: u64,
pub max_batch_size: usize,
pub max_task_age_secs: u64,
pub max_effect_retries: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct TaskPlannerConfig {
pub batch_interval_ms: u64,
pub poll_interval_ms: u64,
}
impl TaskPlannerConfig {
pub fn poll_interval(&self) -> Duration {
Duration::from_millis(self.poll_interval_ms.max(10))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AdmissionOutcome {
Accepted(SubmissionResource),
Existing(SubmissionResource),
Conflict(SubmissionResource),
}
pub trait TaskSubmissionStore: std::fmt::Debug + Send + Sync {
type Error: Error + Send + Sync + 'static;
fn admit(
&self,
request: &TaskSubmissionRequest,
policy: &TaskChainPolicy,
) -> impl Future<Output = Result<AdmissionOutcome, Self::Error>> + Send;
fn submission(
&self,
id: crate::SubmissionId,
) -> impl Future<Output = Result<Option<SubmissionResource>, Self::Error>> + Send;
fn events(
&self,
after: EventCursor,
limit: u32,
wait: Duration,
) -> impl Future<Output = Result<SubmissionEventPage, Self::Error>> + Send;
}
pub trait TaskPlannerObserver: std::fmt::Debug + Send + Sync {
fn heartbeat(&self);
fn failed(&self, error: String);
fn planned(&self, chain_id: u64, operation: TaskOperation);
}
#[derive(Debug)]
pub struct TaskSubmissionService<S> {
store: Arc<S>,
chains: HashMap<u64, TaskChainPolicy>,
}
impl<S> TaskSubmissionService<S>
where
S: TaskSubmissionStore,
{
pub fn new(store: Arc<S>, chains: HashMap<u64, TaskChainPolicy>) -> Self {
Self { store, chains }
}
pub fn chains(&self) -> &HashMap<u64, TaskChainPolicy> {
&self.chains
}
pub async fn admit(
&self,
request: &TaskSubmissionRequest,
) -> Result<AdmissionOutcome, TaskSubmissionError<S::Error>> {
request
.validate()
.map_err(|error| TaskSubmissionError::Invalid(error.to_string()))?;
let policy = self
.chains
.get(&request.payload.chain_id)
.ok_or(TaskSubmissionError::UnsupportedChain)?;
let expected_key = derive_idempotency_key(
request.producer_id.as_bytes(),
policy.chain_id,
request.payload.operation,
request.payload.task_id,
request.payload.task_response_digest,
);
if request.idempotency_key != expected_key {
return Err(TaskSubmissionError::Invalid(
"idempotency key does not match the producer and payload".to_string(),
));
}
self.store
.admit(request, policy)
.await
.map_err(TaskSubmissionError::Store)
}
pub async fn status(
&self,
id: crate::SubmissionId,
) -> Result<Option<SubmissionResource>, TaskSubmissionError<S::Error>> {
self.store.submission(id).await.map_err(TaskSubmissionError::Store)
}
pub async fn events(
&self,
after: EventCursor,
limit: u32,
wait: Duration,
) -> Result<SubmissionEventPage, TaskSubmissionError<S::Error>> {
self.store
.events(after, limit, wait)
.await
.map_err(TaskSubmissionError::Store)
}
}
#[derive(Debug, thiserror::Error)]
pub enum TaskSubmissionError<E: Error + 'static> {
#[error("invalid request: {0}")]
Invalid(String),
#[error("unsupported chain")]
UnsupportedChain,
#[error(transparent)]
Store(E),
}