use anyhow::{Result, anyhow, ensure};
use rusqlite::Connection;
use serde::Serialize;
use std::path::{Path, PathBuf};
use super::store::{StoreDialect, preflight_existing_store};
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum WorkflowWorkKind {
Run,
QueueItem,
Effect,
Invocation,
PauseRequest,
GraphBarrier,
}
impl WorkflowWorkKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::Run => "workflow-run",
Self::QueueItem => "workflow-queue-item",
Self::Effect => "workflow-effect",
Self::Invocation => "workflow-invocation",
Self::PauseRequest => "workflow-pause-request",
Self::GraphBarrier => "workflow-graph-barrier",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowWorkBlocker {
pub kind: WorkflowWorkKind,
pub graph_id: String,
pub identity: String,
pub state: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UnfinishedWorkflowWork {
pub blockers: Vec<WorkflowWorkBlocker>,
pub truncated: bool,
}
impl UnfinishedWorkflowWork {
pub fn empty() -> Self {
Self {
blockers: Vec::new(),
truncated: false,
}
}
pub fn is_idle(&self) -> bool {
self.blockers.is_empty()
}
pub fn blockers(&self) -> &[WorkflowWorkBlocker] {
&self.blockers
}
}
pub const MAX_UNFINISHED_WORKFLOW_WORK: usize = 128;
impl StoreDialect {
pub fn database_path(&self, portable_root: &Path) -> PathBuf {
self.state_dir(portable_root).join("strategies.sqlite3")
}
}
pub fn read_unfinished_local_work(
portable_root: &Path,
dialect: &StoreDialect,
) -> Result<UnfinishedWorkflowWork> {
let path = dialect.database_path(portable_root);
if !path.is_file() {
return Ok(UnfinishedWorkflowWork::empty());
}
preflight_existing_store(&path)?;
let connection = Connection::open_with_flags(
&path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
)?;
for table in [
"strategy_runs",
"strategy_commands",
"workflow_queue",
"workflow_invocations",
"workflow_pause_requests",
"workflow_graph_state",
] {
let exists: bool = connection.query_row(
"SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name=?1)",
[table],
|row| row.get(0),
)?;
ensure!(exists, "workflow_schema_migration_required");
}
unfinished_local_work(&connection)
}
fn unfinished_local_work(connection: &Connection) -> Result<UnfinishedWorkflowWork> {
let sql = format!(
"SELECT kind, graph_id, identity, state FROM (
SELECT '{run}' AS kind, run_id AS graph_id, run_id AS identity,
'unfinished' AS state
FROM strategy_runs
WHERE COALESCE(terminal, 0)=0
UNION ALL
SELECT '{queue}', '', item_id, status
FROM workflow_queue
WHERE status IN ('pending','claimed')
UNION ALL
SELECT '{effect}', run_id, command_id, status
FROM strategy_commands
WHERE status IN ('claimed','running','cancel-requested','in-doubt')
UNION ALL
SELECT '{invocation}', graph_id, node_id||'#'||invocation_id, 'unsettled'
FROM workflow_invocations
WHERE settled=0
UNION ALL
SELECT '{pause}', graph_id, target, 'pause-requested'
FROM workflow_pause_requests
WHERE active=1
UNION ALL
SELECT '{barrier}', graph_id, 'graph', 'barrier-active'
FROM workflow_graph_state
WHERE barrier_active=1
)
ORDER BY kind, graph_id, identity
LIMIT {limit}",
run = WorkflowWorkKind::Run.as_str(),
queue = WorkflowWorkKind::QueueItem.as_str(),
effect = WorkflowWorkKind::Effect.as_str(),
invocation = WorkflowWorkKind::Invocation.as_str(),
pause = WorkflowWorkKind::PauseRequest.as_str(),
barrier = WorkflowWorkKind::GraphBarrier.as_str(),
limit = MAX_UNFINISHED_WORKFLOW_WORK + 1,
);
let mut statement = connection.prepare(&sql)?;
let rows = statement
.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, String>(3)?,
))
})?
.collect::<rusqlite::Result<Vec<_>>>()?;
let truncated = rows.len() > MAX_UNFINISHED_WORKFLOW_WORK;
let mut blockers = Vec::with_capacity(rows.len().min(MAX_UNFINISHED_WORKFLOW_WORK));
for (kind, graph_id, identity, state) in rows.into_iter().take(MAX_UNFINISHED_WORKFLOW_WORK) {
let kind = match kind.as_str() {
"workflow-run" => WorkflowWorkKind::Run,
"workflow-queue-item" => WorkflowWorkKind::QueueItem,
"workflow-effect" => WorkflowWorkKind::Effect,
"workflow-invocation" => WorkflowWorkKind::Invocation,
"workflow-pause-request" => WorkflowWorkKind::PauseRequest,
"workflow-graph-barrier" => WorkflowWorkKind::GraphBarrier,
other => return Err(anyhow!("workflow_work_kind_unknown: {other}")),
};
blockers.push(WorkflowWorkBlocker {
kind,
graph_id,
identity,
state,
});
}
Ok(UnfinishedWorkflowWork {
blockers,
truncated,
})
}