use std::sync::Arc;
use async_trait::async_trait;
use dataflow_rs::engine::error::DataflowError;
use dataflow_rs::engine::functions::AsyncFunctionHandler;
use dataflow_rs::engine::task_context::TaskContext;
use dataflow_rs::engine::task_outcome::TaskOutcome;
use serde_json::Value;
use super::connector_helpers::{
ConnectorCall, json_type_name, require_cache_connector, require_op, resolve_required_str,
resolve_value, to_connect_error, to_exec_error,
};
use super::schema::{FieldKind, FieldSchema};
use crate::connector::ConnectorRegistry;
use crate::connector::cache_backend::{CachePool, CachePurpose};
const NAME: &str = "cache_write";
pub struct CacheWriteHandler {
pub cache_pool: Arc<CachePool>,
pub registry: Arc<ConnectorRegistry>,
}
#[async_trait]
impl AsyncFunctionHandler for CacheWriteHandler {
type Input = Value;
async fn execute(
&self,
ctx: &mut TaskContext<'_>,
input: &Value,
) -> dataflow_rs::Result<TaskOutcome> {
let call = ConnectorCall::begin(NAME, input, ctx)?;
let key = resolve_required_str(input, "key", call.name, ctx)?;
let value = match input.get("value") {
Some(v) => resolve_value(v, ctx),
None => {
return Err(DataflowError::Validation(format!(
"{} requires 'value'",
call.name
)));
}
};
let ttl = resolve_ttl_secs(input, ctx)?;
call.run(&self.registry, async {
let connector_config = call.resolve(&self.registry, None).await?;
let cache_config = require_cache_connector(&connector_config, call.connector)?;
require_op(cache_config.operations.write, "write", call.connector)?;
let backend = self
.cache_pool
.get_backend(CachePurpose::Workflow, call.connector, cache_config)
.await
.map_err(to_connect_error)?;
let value_str = serde_json::to_string(&value).map_err(|e| {
DataflowError::Validation(format!("Failed to serialize value for cache: {e}"))
})?;
if let Some(ttl) = ttl {
backend
.set_ex(&key, &value_str, ttl)
.await
.map_err(to_exec_error)?;
} else {
backend.set(&key, &value_str).await.map_err(to_exec_error)?;
}
tracing::debug!(
key = %key,
ttl = ?ttl,
"Wrote value to cache"
);
Ok(TaskOutcome::Success)
})
.await
}
}
fn resolve_ttl_secs(input: &Value, ctx: &TaskContext<'_>) -> Result<Option<u64>, DataflowError> {
let Some(raw) = input.get("ttl_secs") else {
return Ok(None);
};
match resolve_value(raw, ctx) {
Value::Null => Ok(None),
Value::Number(n) => {
if let Some(u) = n.as_u64() {
Ok(Some(u))
} else if let Some(f) = n.as_f64()
&& f >= 0.0
&& f.fract() == 0.0
{
Ok(Some(f as u64))
} else {
Err(DataflowError::Validation(format!(
"{NAME} 'ttl_secs' must be a non-negative whole number of seconds, got {n}"
)))
}
}
other => Err(DataflowError::Validation(format!(
"{NAME} 'ttl_secs' must resolve to a number, got {}",
json_type_name(&other)
))),
}
}
pub(super) const CACHE_WRITE_FIELDS: &[FieldSchema] = &[
FieldSchema {
name: "connector",
description: "Name of the cache connector to write to.",
kind: FieldKind::String,
required: true,
resolvable: false,
alias: None,
},
FieldSchema {
name: "key",
description: "Cache key to set. Accepts {\"var\": \"path\"} to read the value from the message.",
kind: FieldKind::String,
required: true,
resolvable: true,
alias: None,
},
FieldSchema {
name: "value",
description: "Value to store. May be any JSON value. Accepts {\"var\": \"path\"} to read the value from the message.",
kind: FieldKind::Any,
required: true,
resolvable: true,
alias: None,
},
FieldSchema {
name: "ttl_secs",
description: "Time-to-live in seconds. Omit for no expiry. Accepts {\"var\": \"path\"} to read the value from the message.",
kind: FieldKind::Number,
required: false,
resolvable: true,
alias: None,
},
];