use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
use arrow_array::RecordBatch;
use async_trait::async_trait;
use serde::{Serialize, de::DeserializeOwned};
use serde_json::Value;
use tokio::sync::watch;
use tokio::task::{AbortHandle, JoinHandle};
use crate::database::JobDescription;
use crate::error::{Error, JobFailure, Result};
#[derive(Debug, Clone, Default)]
pub struct JobEventsRequest {
pub limit: Option<u32>,
pub filter: Option<String>,
}
impl JobEventsRequest {
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
pub fn filter(mut self, filter: impl Into<String>) -> Self {
self.filter = Some(filter.into());
self
}
}
fn job_detail_not_supported<T>(what: &str) -> Result<T> {
Err(Error::NotSupported {
message: format!("{what} is only available for server-side jobs"),
})
}
#[async_trait]
pub(crate) trait JobHandle: Send + Sync {
fn id(&self) -> Option<&str> {
None
}
async fn status(&self) -> Result<String>;
async fn wait(&self) -> Result<TerminalResult>;
async fn cancel(&self) -> Result<()>;
async fn describe(&self) -> Result<JobDescription> {
job_detail_not_supported("describing a job")
}
async fn events(&self, _request: JobEventsRequest) -> Result<Vec<RecordBatch>> {
job_detail_not_supported("job event history")
}
}
#[derive(Clone)]
pub(crate) struct TerminalResult {
value: Option<Value>,
request_id: Option<String>,
}
impl TerminalResult {
fn local(value: Value) -> Self {
Self {
value: Some(value),
request_id: None,
}
}
pub(crate) fn remote(value: Option<Value>, request_id: String) -> Self {
Self {
value,
request_id: Some(request_id),
}
}
pub(crate) fn value(&self) -> Option<&Value> {
self.value.as_ref()
}
fn decode<T: DeserializeOwned>(self) -> Result<T> {
let value = self.value.ok_or_else(|| match &self.request_id {
Some(request_id) => Error::Http {
source: "successful typed job response did not contain a result".into(),
request_id: request_id.clone(),
status_code: None,
},
None => Error::Runtime {
message: "successful typed job did not contain a result".to_string(),
},
})?;
serde_json::from_value(value).map_err(|error| match self.request_id {
Some(request_id) => Error::Http {
source: format!("failed to parse typed job result: {error}").into(),
request_id,
status_code: None,
},
None => Error::Runtime {
message: format!("failed to parse typed job result: {error}"),
},
})
}
}
type ResultDecoder<T> = Arc<dyn Fn(TerminalResult) -> Result<T> + Send + Sync>;
enum JobInner<T> {
Handle {
handle: Box<dyn JobHandle>,
decode: ResultDecoder<T>,
},
Completed(T),
}
#[derive(Default)]
struct JobCache {
state: Option<String>,
description: Option<JobDescription>,
}
pub struct Job<T = ()>
where
T: Clone + Send + Sync + 'static,
{
inner: JobInner<T>,
cache: RwLock<JobCache>,
}
impl<T> std::fmt::Debug for Job<T>
where
T: Clone + Send + Sync + 'static,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let cache = self.cache_read();
let mut out = f.debug_struct("Job");
out.field("id", &self.id())
.field("done", &matches!(self.inner, JobInner::Completed(_)));
if let Some(state) = &cache.state {
out.field("state", state);
}
if let Some(description) = &cache.description {
out.field("job_type", &description.job_type)
.field("creation_ms", &description.creation_ms);
if !description.spec.is_null() {
out.field("spec", &description.spec);
}
if let Some(result) = &description.result {
out.field("result", result);
}
if let Some(failure) = &description.failure {
out.field("failure", failure);
}
}
out.finish()
}
}
impl Job<()> {
pub(crate) fn new_done() -> Self {
Self {
inner: JobInner::Completed(()),
cache: RwLock::new(JobCache {
state: Some("finished".to_string()),
description: None,
}),
}
}
pub(crate) fn new(handle: Box<dyn JobHandle>) -> Self {
Self {
inner: JobInner::Handle {
handle,
decode: Arc::new(|_| Ok(())),
},
cache: RwLock::default(),
}
}
pub(crate) fn opened(handle: Box<dyn JobHandle>, description: JobDescription) -> Self {
let job = Self::new(handle);
{
let mut cache = job.cache_write();
cache.state = Some(description.state.clone());
cache.description = Some(description);
}
job
}
}
impl<T> Job<T>
where
T: Clone + DeserializeOwned + Send + Sync + 'static,
{
pub(crate) fn new_typed(handle: Box<dyn JobHandle>) -> Self {
Self {
inner: JobInner::Handle {
handle,
decode: Arc::new(TerminalResult::decode::<T>),
},
cache: RwLock::default(),
}
}
}
impl<T> Job<T>
where
T: Clone + Serialize + DeserializeOwned + Send + Sync + 'static,
{
pub(crate) fn spawned(task: JoinHandle<Result<T>>) -> Self {
Self::new_typed(Box::new(SpawnedJob::new(task)))
}
}
impl<T> Job<T>
where
T: Clone + Send + Sync + 'static,
{
pub fn id(&self) -> Option<&str> {
match &self.inner {
JobInner::Handle { handle, .. } => handle.id(),
JobInner::Completed(_) => None,
}
}
fn cache_read(&self) -> RwLockReadGuard<'_, JobCache> {
self.cache.read().unwrap_or_else(|err| err.into_inner())
}
fn cache_write(&self) -> RwLockWriteGuard<'_, JobCache> {
self.cache.write().unwrap_or_else(|err| err.into_inner())
}
pub async fn refresh(&self) -> Result<()> {
self.refresh_state().await.map(|_| ())
}
async fn refresh_state(&self) -> Result<String> {
let JobInner::Handle { handle, .. } = &self.inner else {
let state = "finished".to_string();
self.cache_write().state = Some(state.clone());
return Ok(state);
};
match handle.describe().await {
Ok(description) => {
let state = description.state.clone();
let mut cache = self.cache_write();
cache.state = Some(state.clone());
cache.description = Some(description);
Ok(state)
}
Err(Error::NotSupported { .. }) => {
let state = handle.status().await?;
self.cache_write().state = Some(state.clone());
Ok(state)
}
Err(err) => Err(err),
}
}
pub async fn status(&self) -> Result<String> {
self.refresh_state().await
}
pub fn state(&self) -> Option<String> {
self.cache_read().state.clone()
}
pub fn description(&self) -> Option<JobDescription> {
self.cache_read().description.clone()
}
pub fn job_type(&self) -> Option<String> {
self.with_description(|description| description.job_type.clone())
}
pub fn creation_ms(&self) -> Option<i64> {
self.with_description(|description| description.creation_ms)
}
pub fn spec(&self) -> Option<Value> {
self.with_description(|description| description.spec.clone())
.filter(|spec| !spec.is_null())
}
pub fn result(&self) -> Option<Value> {
self.with_description(|description| description.result.clone())
.flatten()
}
pub fn failure(&self) -> Option<JobFailure> {
self.with_description(|description| description.failure.clone())
.flatten()
}
fn with_description<R>(&self, read: impl FnOnce(&JobDescription) -> R) -> Option<R> {
self.cache_read().description.as_ref().map(read)
}
pub async fn events(&self, request: JobEventsRequest) -> Result<Vec<RecordBatch>> {
match &self.inner {
JobInner::Handle { handle, .. } => handle.events(request).await,
JobInner::Completed(_) => Err(Error::NotSupported {
message: "this operation completed before its handle was created, so it \
carries no job id to query events with"
.to_string(),
}),
}
}
pub async fn wait(&self) -> Result<T> {
match &self.inner {
JobInner::Handle { handle, decode } => {
let settled = handle.wait().await;
if let Some(state) = terminal_state(&settled) {
self.cache_write().state = Some(state.to_string());
}
(decode)(settled?)
}
JobInner::Completed(result) => {
self.cache_write().state = Some("finished".to_string());
Ok(result.clone())
}
}
}
pub async fn cancel(&self) -> Result<()> {
match &self.inner {
JobInner::Handle { handle, .. } => handle.cancel().await,
JobInner::Completed(_) => Ok(()),
}
}
pub fn map<U, F>(self, map: F) -> Job<U>
where
U: Clone + Send + Sync + 'static,
F: Fn(T) -> U + Send + Sync + 'static,
{
let Self { inner, cache } = self;
match inner {
JobInner::Handle { handle, decode } => Job {
inner: JobInner::Handle {
handle,
decode: Arc::new(move |result| Ok(map((decode)(result)?))),
},
cache,
},
JobInner::Completed(result) => Job {
inner: JobInner::Completed(map(result)),
cache,
},
}
}
}
fn terminal_state(settled: &Result<TerminalResult>) -> Option<&'static str> {
match settled {
Ok(_) => Some("finished"),
Err(Error::JobFailed { .. }) => Some("failed"),
Err(Error::JobCancelled { .. }) => Some("cancelled"),
Err(_) => None,
}
}
#[derive(Clone)]
enum Outcome {
Succeeded(TerminalResult),
Failed(Arc<Error>),
Cancelled,
}
impl Outcome {
fn into_result(self) -> Result<TerminalResult> {
match self {
Self::Succeeded(result) => Ok(result),
Self::Failed(source) => Err(Error::JobFailed {
job_id: None,
failure: JobFailure::from_source(source),
}),
Self::Cancelled => Err(Error::JobCancelled { job_id: None }),
}
}
}
struct SpawnedJob {
outcome: watch::Receiver<Option<Outcome>>,
abort: AbortHandle,
}
impl SpawnedJob {
fn new<T>(task: JoinHandle<Result<T>>) -> Self
where
T: Serialize + Send + 'static,
{
let abort = task.abort_handle();
let (tx, outcome) = watch::channel(None);
tokio::spawn(async move {
let outcome = match task.await {
Ok(Ok(result)) => match serde_json::to_value(result) {
Ok(value) => Outcome::Succeeded(TerminalResult::local(value)),
Err(err) => Outcome::Failed(Arc::new(Error::Runtime {
message: format!("failed to serialize job result: {err}"),
})),
},
Ok(Err(err)) => Outcome::Failed(Arc::new(err)),
Err(err) if err.is_cancelled() => Outcome::Cancelled,
Err(err) => Outcome::Failed(Arc::new(Error::Runtime {
message: format!("job task failed: {err}"),
})),
};
let _ = tx.send(Some(outcome));
});
Self { outcome, abort }
}
}
#[async_trait]
impl JobHandle for SpawnedJob {
async fn status(&self) -> Result<String> {
let label = match &*self.outcome.borrow() {
None => "running",
Some(Outcome::Succeeded(_)) => "finished",
Some(Outcome::Failed(_)) => "failed",
Some(Outcome::Cancelled) => "cancelled",
};
Ok(label.to_string())
}
async fn wait(&self) -> Result<TerminalResult> {
let mut outcome = self.outcome.clone();
let settled = outcome
.wait_for(|outcome| outcome.is_some())
.await
.map_err(|_| Error::Runtime {
message: "job outcome was dropped before it completed".to_string(),
})?
.clone()
.expect("wait_for returns once an outcome is set");
settled.into_result()
}
async fn cancel(&self) -> Result<()> {
self.abort.abort();
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::future::pending;
use super::*;
#[tokio::test]
async fn mapped_spawned_job_reuses_outcome() {
let job = Job::spawned(tokio::spawn(async { Ok(41_u64) })).map(|value| value + 1);
assert_eq!(job.wait().await.unwrap(), 42);
assert_eq!(job.wait().await.unwrap(), 42);
assert_eq!(job.status().await.unwrap(), "finished");
}
#[tokio::test]
async fn mapped_spawned_job_preserves_cancellation() {
let job = Job::spawned(tokio::spawn(async { pending::<Result<u64>>().await }))
.map(|value| value.to_string());
job.cancel().await.unwrap();
assert!(matches!(job.wait().await, Err(Error::JobCancelled { .. })));
assert_eq!(job.status().await.unwrap(), "cancelled");
}
}