Use hero-job crate instead of local job module
This commit is contained in:
18
Cargo.lock
generated
18
Cargo.lock
generated
@@ -1283,6 +1283,23 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "hero-job"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"hex",
|
||||
"log",
|
||||
"redis 0.25.4",
|
||||
"secp256k1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hero_logger"
|
||||
version = "0.1.0"
|
||||
@@ -3244,6 +3261,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"env_logger",
|
||||
"hero-job",
|
||||
"hero_logger",
|
||||
"heromodels",
|
||||
"heromodels-derive",
|
||||
|
||||
@@ -52,6 +52,8 @@ hex = "0.4"
|
||||
rand = "0.8"
|
||||
# Core hero dependencies
|
||||
hero_logger = { git = "https://git.ourworld.tf/herocode/baobab.git", branch = "logger" }
|
||||
hero-job = { path = "../job/rust" }
|
||||
# hero-job = { git = "https://git.ourworld.tf/herocode/job.git", subdirectory = "rust" }
|
||||
|
||||
# Osiris dependencies (used by runner_osiris binary)
|
||||
osiris = { package = "osiris-core", path = "../osiris/core" }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::job::Job;
|
||||
use crate::Job;
|
||||
use log::{debug, error, info};
|
||||
use rhai::{Engine, packages::Package};
|
||||
use std::collections::HashMap;
|
||||
|
||||
462
src/client.rs
462
src/client.rs
@@ -1,462 +0,0 @@
|
||||
//! Job client implementation for managing jobs in Redis
|
||||
|
||||
use chrono::Utc;
|
||||
use redis::AsyncCommands;
|
||||
use crate::job::{Job, JobStatus, JobError};
|
||||
|
||||
/// Client for managing jobs in Redis
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Client {
|
||||
redis_client: redis::Client,
|
||||
namespace: String,
|
||||
}
|
||||
|
||||
pub struct ClientBuilder {
|
||||
/// Redis URL for connection
|
||||
redis_url: String,
|
||||
/// Namespace for queue keys
|
||||
namespace: String,
|
||||
}
|
||||
|
||||
impl ClientBuilder {
|
||||
/// Create a new client builder
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
redis_url: "redis://localhost:6379".to_string(),
|
||||
namespace: "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the Redis URL
|
||||
pub fn redis_url<S: Into<String>>(mut self, url: S) -> Self {
|
||||
self.redis_url = url.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the namespace for queue keys
|
||||
pub fn namespace<S: Into<String>>(mut self, namespace: S) -> Self {
|
||||
self.namespace = namespace.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the client
|
||||
pub async fn build(self) -> Result<Client, JobError> {
|
||||
// Create Redis client
|
||||
let redis_client = redis::Client::open(self.redis_url.as_str())
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
Ok(Client {
|
||||
redis_client,
|
||||
namespace: self.namespace,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Client {
|
||||
fn default() -> Self {
|
||||
// Note: Default implementation creates an empty client
|
||||
// Use Client::builder() for proper initialization
|
||||
Self {
|
||||
redis_client: redis::Client::open("redis://localhost:6379").unwrap(),
|
||||
namespace: "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client builder
|
||||
pub fn builder() -> ClientBuilder {
|
||||
ClientBuilder::new()
|
||||
}
|
||||
|
||||
/// List all job IDs from Redis
|
||||
pub async fn list_jobs(&self) -> Result<Vec<String>, JobError> {
|
||||
let mut conn = self.redis_client
|
||||
.get_multiplexed_async_connection()
|
||||
.await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
let keys: Vec<String> = conn.keys(format!("{}:*", &self.jobs_key())).await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
let job_ids: Vec<String> = keys
|
||||
.into_iter()
|
||||
.filter_map(|key| {
|
||||
if key.starts_with(&format!("{}:", self.jobs_key())) {
|
||||
key.strip_prefix(&format!("{}:", self.jobs_key()))
|
||||
.map(|s| s.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(job_ids)
|
||||
}
|
||||
|
||||
fn jobs_key(&self) -> String {
|
||||
if self.namespace.is_empty() {
|
||||
format!("job")
|
||||
} else {
|
||||
format!("{}:job", self.namespace)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn job_key(&self, job_id: &str) -> String {
|
||||
if self.namespace.is_empty() {
|
||||
format!("job:{}", job_id)
|
||||
} else {
|
||||
format!("{}:job:{}", self.namespace, job_id)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn job_reply_key(&self, job_id: &str) -> String {
|
||||
if self.namespace.is_empty() {
|
||||
format!("reply:{}", job_id)
|
||||
} else {
|
||||
format!("{}:reply:{}", self.namespace, job_id)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn runner_key(&self, runner_name: &str) -> String {
|
||||
if self.namespace.is_empty() {
|
||||
format!("runner:{}", runner_name)
|
||||
} else {
|
||||
format!("{}:runner:{}", self.namespace, runner_name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set job error in Redis
|
||||
pub async fn set_error(&self,
|
||||
job_id: &str,
|
||||
error: &str,
|
||||
) -> Result<(), JobError> {
|
||||
let job_key = self.job_key(job_id);
|
||||
let now = Utc::now();
|
||||
|
||||
let mut conn = self.redis_client
|
||||
.get_multiplexed_async_connection()
|
||||
.await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
let _: () = conn.hset_multiple(&job_key, &[
|
||||
("error", error),
|
||||
("status", JobStatus::Error.as_str()),
|
||||
("updated_at", &now.to_rfc3339()),
|
||||
]).await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set job status in Redis
|
||||
pub async fn set_job_status(&self,
|
||||
job_id: &str,
|
||||
status: JobStatus,
|
||||
) -> Result<(), JobError> {
|
||||
let job_key = self.job_key(job_id);
|
||||
let now = Utc::now();
|
||||
|
||||
let mut conn = self.redis_client
|
||||
.get_multiplexed_async_connection()
|
||||
.await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
let _: () = conn.hset_multiple(&job_key, &[
|
||||
("status", status.as_str()),
|
||||
("updated_at", &now.to_rfc3339()),
|
||||
]).await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get job status from Redis
|
||||
pub async fn get_status(
|
||||
&self,
|
||||
job_id: &str,
|
||||
) -> Result<JobStatus, JobError> {
|
||||
let mut conn = self.redis_client
|
||||
.get_multiplexed_async_connection()
|
||||
.await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
let status_str: Option<String> = conn.hget(&self.job_key(job_id), "status").await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
match status_str {
|
||||
Some(s) => JobStatus::from_str(&s).ok_or_else(|| JobError::InvalidStatus(s)),
|
||||
None => Err(JobError::NotFound(job_id.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete job from Redis
|
||||
pub async fn delete_from_redis(
|
||||
&self,
|
||||
job_id: &str,
|
||||
) -> Result<(), JobError> {
|
||||
let mut conn = self.redis_client
|
||||
.get_multiplexed_async_connection()
|
||||
.await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
let job_key = self.job_key(job_id);
|
||||
let _: () = conn.del(&job_key).await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Store this job in Redis with the specified status
|
||||
pub async fn store_job_in_redis_with_status(&self, job: &Job, status: JobStatus) -> Result<(), JobError> {
|
||||
let mut conn = self.redis_client
|
||||
.get_multiplexed_async_connection()
|
||||
.await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
let job_key = self.job_key(&job.id);
|
||||
|
||||
// Serialize the job data
|
||||
let job_data = serde_json::to_string(job)
|
||||
.map_err(|e| JobError::Serialization(e))?;
|
||||
|
||||
// Store job data in Redis hash
|
||||
let _: () = conn.hset_multiple(&job_key, &[
|
||||
("data", job_data),
|
||||
("status", status.as_str().to_string()),
|
||||
("created_at", job.created_at.to_rfc3339()),
|
||||
("updated_at", job.updated_at.to_rfc3339()),
|
||||
]).await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
// Set TTL for the job (24 hours)
|
||||
let _: () = conn.expire(&job_key, 86400).await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Store this job in Redis (defaults to Dispatched status for backwards compatibility)
|
||||
pub async fn store_job_in_redis(&self, job: &Job) -> Result<(), JobError> {
|
||||
self.store_job_in_redis_with_status(job, JobStatus::Dispatched).await
|
||||
}
|
||||
|
||||
/// Load a job from Redis by ID
|
||||
pub async fn load_job_from_redis(
|
||||
&self,
|
||||
job_id: &str,
|
||||
) -> Result<Job, JobError> {
|
||||
let job_key = self.job_key(job_id);
|
||||
|
||||
let mut conn = self.redis_client
|
||||
.get_multiplexed_async_connection()
|
||||
.await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
// Get job data from Redis
|
||||
let job_data: Option<String> = conn.hget(&job_key, "data").await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
match job_data {
|
||||
Some(data) => {
|
||||
let job: Job = serde_json::from_str(&data)
|
||||
.map_err(|e| JobError::Serialization(e))?;
|
||||
Ok(job)
|
||||
}
|
||||
None => Err(JobError::NotFound(job_id.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a job by ID
|
||||
pub async fn delete_job(&mut self, job_id: &str) -> Result<(), JobError> {
|
||||
let mut conn = self.redis_client.get_multiplexed_async_connection().await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
let job_key = self.job_key(job_id);
|
||||
let deleted_count: i32 = conn.del(&job_key).await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
if deleted_count == 0 {
|
||||
return Err(JobError::NotFound(job_id.to_string()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set job result in Redis
|
||||
pub async fn set_result(
|
||||
&self,
|
||||
job_id: &str,
|
||||
result: &str,
|
||||
) -> Result<(), JobError> {
|
||||
let job_key = self.job_key(&job_id);
|
||||
let now = Utc::now();
|
||||
let mut conn = self.redis_client
|
||||
.get_multiplexed_async_connection()
|
||||
.await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
let _: () = conn.hset_multiple(&job_key, &[
|
||||
("result", result),
|
||||
("status", JobStatus::Finished.as_str()),
|
||||
("updated_at", &now.to_rfc3339()),
|
||||
]).await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get job result from Redis
|
||||
pub async fn get_result(
|
||||
&self,
|
||||
job_id: &str,
|
||||
) -> Result<Option<String>, JobError> {
|
||||
let job_key = self.job_key(job_id);
|
||||
let mut conn = self.redis_client
|
||||
.get_multiplexed_async_connection()
|
||||
.await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
let result: Option<String> = conn.hget(&job_key, "result").await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Get job result from Redis
|
||||
pub async fn get_error(
|
||||
&self,
|
||||
job_id: &str,
|
||||
) -> Result<Option<String>, JobError> {
|
||||
let job_key = self.job_key(job_id);
|
||||
let mut conn = self.redis_client
|
||||
.get_multiplexed_async_connection()
|
||||
.await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
let result: Option<String> = conn.hget(&job_key, "error").await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Get a job ID from the work queue (blocking pop)
|
||||
pub async fn get_job_id(&self, queue_key: &str) -> Result<Option<String>, JobError> {
|
||||
let mut conn = self.redis_client
|
||||
.get_multiplexed_async_connection()
|
||||
.await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
// Use BRPOP with a short timeout to avoid blocking indefinitely
|
||||
let result: Option<(String, String)> = conn.brpop(queue_key, 1.0).await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
Ok(result.map(|(_, job_id)| job_id))
|
||||
}
|
||||
|
||||
/// Get a job by ID (alias for load_job_from_redis)
|
||||
pub async fn get_job(&self, job_id: &str) -> Result<Job, JobError> {
|
||||
self.load_job_from_redis(job_id).await
|
||||
}
|
||||
|
||||
/// Dispatch a job to a runner's queue
|
||||
pub async fn dispatch_job(&self, job_id: &str, runner_name: &str) -> Result<(), JobError> {
|
||||
let mut conn = self.redis_client
|
||||
.get_multiplexed_async_connection()
|
||||
.await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
let queue_key = self.runner_key(runner_name);
|
||||
|
||||
// Push job ID to the runner's queue (LPUSH for FIFO with BRPOP)
|
||||
let _: () = conn.lpush(&queue_key, job_id).await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run a job: dispatch it, wait for completion, and return the result
|
||||
///
|
||||
/// This is a convenience method that:
|
||||
/// 1. Stores the job in Redis
|
||||
/// 2. Dispatches it to the runner's queue
|
||||
/// 3. Waits for the job to complete (polls status)
|
||||
/// 4. Returns the result or error
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `job` - The job to run
|
||||
/// * `runner_name` - The name of the runner to dispatch to
|
||||
/// * `timeout_secs` - Maximum time to wait for job completion (in seconds)
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(String)` - The job result if successful
|
||||
/// * `Err(JobError)` - If the job fails, times out, or encounters an error
|
||||
pub async fn run_job(
|
||||
&self,
|
||||
job: &crate::job::Job,
|
||||
runner_name: &str,
|
||||
timeout_secs: u64,
|
||||
) -> Result<String, JobError> {
|
||||
use tokio::time::{Duration, timeout};
|
||||
|
||||
// Store the job in Redis
|
||||
self.store_job_in_redis(job).await?;
|
||||
|
||||
// Dispatch to runner queue
|
||||
self.dispatch_job(&job.id, runner_name).await?;
|
||||
|
||||
// Wait for job to complete with timeout
|
||||
let result = timeout(
|
||||
Duration::from_secs(timeout_secs),
|
||||
self.wait_for_job_completion(&job.id)
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(job_result)) => Ok(job_result),
|
||||
Ok(Err(e)) => Err(e),
|
||||
Err(_) => Err(JobError::Timeout(format!(
|
||||
"Job {} did not complete within {} seconds",
|
||||
job.id, timeout_secs
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for a job to complete by polling its status
|
||||
///
|
||||
/// This polls the job status every 500ms until it reaches a terminal state
|
||||
/// (Finished or Error), then returns the result or error.
|
||||
async fn wait_for_job_completion(&self, job_id: &str) -> Result<String, JobError> {
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
loop {
|
||||
// Check job status
|
||||
let status = self.get_status(job_id).await?;
|
||||
|
||||
match status {
|
||||
JobStatus::Finished => {
|
||||
// Job completed successfully, get the result
|
||||
let result = self.get_result(job_id).await?;
|
||||
return result.ok_or_else(|| {
|
||||
JobError::InvalidData(format!("Job {} finished but has no result", job_id))
|
||||
});
|
||||
}
|
||||
JobStatus::Error => {
|
||||
// Job failed, get the error message
|
||||
let mut conn = self.redis_client
|
||||
.get_multiplexed_async_connection()
|
||||
.await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
let error_msg: Option<String> = conn
|
||||
.hget(&self.job_key(job_id), "error")
|
||||
.await
|
||||
.map_err(|e| JobError::Redis(e))?;
|
||||
|
||||
return Err(JobError::InvalidData(
|
||||
error_msg.unwrap_or_else(|| format!("Job {} failed with unknown error", job_id))
|
||||
));
|
||||
}
|
||||
JobStatus::Stopping => {
|
||||
return Err(JobError::InvalidData(format!("Job {} was stopped", job_id)));
|
||||
}
|
||||
// Job is still running (Dispatched, WaitingForPrerequisites, Started)
|
||||
_ => {
|
||||
// Wait before polling again
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
333
src/job.rs
333
src/job.rs
@@ -1,333 +0,0 @@
|
||||
use chrono::{Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
use log::{error};
|
||||
|
||||
pub use crate::client::Client;
|
||||
|
||||
/// Signature for a job - contains the signatory's public key and their signature
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JobSignature {
|
||||
/// Public key of the signatory (hex-encoded secp256k1 public key)
|
||||
pub public_key: String,
|
||||
/// Signature (hex-encoded secp256k1 signature)
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
/// Job status enumeration
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum JobStatus {
|
||||
Created,
|
||||
Dispatched,
|
||||
WaitingForPrerequisites,
|
||||
Started,
|
||||
Error,
|
||||
Stopping,
|
||||
Finished,
|
||||
}
|
||||
|
||||
impl JobStatus {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
JobStatus::Created => "created",
|
||||
JobStatus::Dispatched => "dispatched",
|
||||
JobStatus::WaitingForPrerequisites => "waiting_for_prerequisites",
|
||||
JobStatus::Started => "started",
|
||||
JobStatus::Error => "error",
|
||||
JobStatus::Stopping => "stopping",
|
||||
JobStatus::Finished => "finished",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"created" => Some(JobStatus::Created),
|
||||
"dispatched" => Some(JobStatus::Dispatched),
|
||||
"waiting_for_prerequisites" => Some(JobStatus::WaitingForPrerequisites),
|
||||
"started" => Some(JobStatus::Started),
|
||||
"error" => Some(JobStatus::Error),
|
||||
"stopping" => Some(JobStatus::Stopping),
|
||||
"finished" => Some(JobStatus::Finished),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Representation of a script execution request.
|
||||
///
|
||||
/// This structure contains all the information needed to execute a script
|
||||
/// on a actor service, including the script content, dependencies, and metadata.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Job {
|
||||
pub id: String,
|
||||
pub caller_id: String,
|
||||
pub context_id: String,
|
||||
pub payload: String,
|
||||
pub runner: String, // name of the runner to execute this job
|
||||
pub executor: String, // name of the executor the runner will use to execute this job
|
||||
pub timeout: u64, // timeout in seconds
|
||||
pub env_vars: HashMap<String, String>, // environment variables for script execution
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
|
||||
/// Signatures from authorized signatories (public keys are included in each signature)
|
||||
pub signatures: Vec<JobSignature>,
|
||||
}
|
||||
|
||||
/// Error types for job operations
|
||||
#[derive(Error, Debug)]
|
||||
pub enum JobError {
|
||||
#[error("Redis error: {0}")]
|
||||
Redis(#[from] redis::RedisError),
|
||||
#[error("Serialization error: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
#[error("Job not found: {0}")]
|
||||
NotFound(String),
|
||||
#[error("Invalid job status: {0}")]
|
||||
InvalidStatus(String),
|
||||
#[error("Timeout error: {0}")]
|
||||
Timeout(String),
|
||||
#[error("Invalid job data: {0}")]
|
||||
InvalidData(String),
|
||||
#[error("Signature verification failed: {0}")]
|
||||
SignatureVerificationFailed(String),
|
||||
#[error("Unauthorized: {0}")]
|
||||
Unauthorized(String),
|
||||
}
|
||||
|
||||
impl Job {
|
||||
/// Create a new job with the given parameters
|
||||
pub fn new(
|
||||
caller_id: String,
|
||||
context_id: String,
|
||||
payload: String,
|
||||
runner: String,
|
||||
executor: String,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
caller_id,
|
||||
context_id,
|
||||
payload,
|
||||
runner,
|
||||
executor,
|
||||
timeout: 300, // 5 minutes default
|
||||
env_vars: HashMap::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
signatures: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the canonical representation of the job for signing
|
||||
/// This creates a deterministic string representation that can be hashed and signed
|
||||
/// Note: Signatures are excluded from the canonical representation
|
||||
pub fn canonical_representation(&self) -> String {
|
||||
// Create a deterministic representation excluding signatures
|
||||
// Sort env_vars keys for deterministic ordering
|
||||
let mut env_vars_sorted: Vec<_> = self.env_vars.iter().collect();
|
||||
env_vars_sorted.sort_by_key(|&(k, _)| k);
|
||||
|
||||
format!(
|
||||
"{}:{}:{}:{}:{}:{}:{}:{:?}",
|
||||
self.id,
|
||||
self.caller_id,
|
||||
self.context_id,
|
||||
self.payload,
|
||||
self.runner,
|
||||
self.executor,
|
||||
self.timeout,
|
||||
env_vars_sorted
|
||||
)
|
||||
}
|
||||
|
||||
/// Get list of signatory public keys from signatures
|
||||
pub fn signatories(&self) -> Vec<String> {
|
||||
self.signatures.iter()
|
||||
.map(|sig| sig.public_key.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Verify that all signatures are valid
|
||||
/// Returns Ok(()) if verification passes, Err otherwise
|
||||
/// Empty signatures list is allowed - loop simply won't execute
|
||||
pub fn verify_signatures(&self) -> Result<(), JobError> {
|
||||
use secp256k1::{Message, PublicKey, Secp256k1, ecdsa::Signature};
|
||||
use sha2::{Sha256, Digest};
|
||||
|
||||
// Get the canonical representation and hash it
|
||||
let canonical = self.canonical_representation();
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(canonical.as_bytes());
|
||||
let hash = hasher.finalize();
|
||||
|
||||
let secp = Secp256k1::verification_only();
|
||||
let message = Message::from_digest_slice(&hash)
|
||||
.map_err(|e| JobError::SignatureVerificationFailed(format!("Invalid message: {}", e)))?;
|
||||
|
||||
// Verify each signature (if any)
|
||||
for sig_data in &self.signatures {
|
||||
// Decode public key
|
||||
let pubkey_bytes = hex::decode(&sig_data.public_key)
|
||||
.map_err(|e| JobError::SignatureVerificationFailed(format!("Invalid public key hex: {}", e)))?;
|
||||
let pubkey = PublicKey::from_slice(&pubkey_bytes)
|
||||
.map_err(|e| JobError::SignatureVerificationFailed(format!("Invalid public key: {}", e)))?;
|
||||
|
||||
// Decode signature
|
||||
let sig_bytes = hex::decode(&sig_data.signature)
|
||||
.map_err(|e| JobError::SignatureVerificationFailed(format!("Invalid signature hex: {}", e)))?;
|
||||
let signature = Signature::from_compact(&sig_bytes)
|
||||
.map_err(|e| JobError::SignatureVerificationFailed(format!("Invalid signature: {}", e)))?;
|
||||
|
||||
// Verify signature
|
||||
secp.verify_ecdsa(&message, &signature, &pubkey)
|
||||
.map_err(|e| JobError::SignatureVerificationFailed(format!("Signature verification failed: {}", e)))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for constructing job execution requests.
|
||||
pub struct JobBuilder {
|
||||
caller_id: String,
|
||||
context_id: String,
|
||||
payload: String,
|
||||
runner: String,
|
||||
executor: String,
|
||||
timeout: u64, // timeout in seconds
|
||||
env_vars: HashMap<String, String>,
|
||||
signatures: Vec<JobSignature>,
|
||||
}
|
||||
|
||||
impl JobBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
caller_id: "".to_string(),
|
||||
context_id: "".to_string(),
|
||||
payload: "".to_string(),
|
||||
runner: "".to_string(),
|
||||
executor: "".to_string(),
|
||||
timeout: 300, // 5 minutes default
|
||||
env_vars: HashMap::new(),
|
||||
signatures: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the caller ID for this job
|
||||
pub fn caller_id(mut self, caller_id: &str) -> Self {
|
||||
self.caller_id = caller_id.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the context ID for this job
|
||||
pub fn context_id(mut self, context_id: &str) -> Self {
|
||||
self.context_id = context_id.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the payload (script content) for this job
|
||||
pub fn payload(mut self, payload: &str) -> Self {
|
||||
self.payload = payload.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the runner name for this job
|
||||
pub fn runner(mut self, runner: &str) -> Self {
|
||||
self.runner = runner.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the executor for this job
|
||||
pub fn executor(mut self, executor: &str) -> Self {
|
||||
self.executor = executor.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the timeout for job execution (in seconds)
|
||||
pub fn timeout(mut self, timeout: u64) -> Self {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a single environment variable
|
||||
pub fn env_var(mut self, key: &str, value: &str) -> Self {
|
||||
self.env_vars.insert(key.to_string(), value.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set multiple environment variables from a HashMap
|
||||
pub fn env_vars(mut self, env_vars: HashMap<String, String>) -> Self {
|
||||
self.env_vars = env_vars;
|
||||
self
|
||||
}
|
||||
|
||||
/// Clear all environment variables
|
||||
pub fn clear_env_vars(mut self) -> Self {
|
||||
self.env_vars.clear();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a signature (public key and signature)
|
||||
pub fn signature(mut self, public_key: &str, signature: &str) -> Self {
|
||||
self.signatures.push(JobSignature {
|
||||
public_key: public_key.to_string(),
|
||||
signature: signature.to_string(),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Set multiple signatures
|
||||
pub fn signatures(mut self, signatures: Vec<JobSignature>) -> Self {
|
||||
self.signatures = signatures;
|
||||
self
|
||||
}
|
||||
|
||||
/// Clear all signatures
|
||||
pub fn clear_signatures(mut self) -> Self {
|
||||
self.signatures.clear();
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the job
|
||||
pub fn build(self) -> Result<Job, JobError> {
|
||||
if self.caller_id.is_empty() {
|
||||
return Err(JobError::InvalidData("caller_id is required".to_string()));
|
||||
}
|
||||
if self.context_id.is_empty() {
|
||||
return Err(JobError::InvalidData("context_id is required".to_string()));
|
||||
}
|
||||
if self.payload.is_empty() {
|
||||
return Err(JobError::InvalidData("payload is required".to_string()));
|
||||
}
|
||||
if self.runner.is_empty() {
|
||||
return Err(JobError::InvalidData("runner is required".to_string()));
|
||||
}
|
||||
if self.executor.is_empty() {
|
||||
return Err(JobError::InvalidData("executor is required".to_string()));
|
||||
}
|
||||
|
||||
let mut job = Job::new(
|
||||
self.caller_id,
|
||||
self.context_id,
|
||||
self.payload,
|
||||
self.runner,
|
||||
self.executor,
|
||||
);
|
||||
|
||||
job.timeout = self.timeout;
|
||||
job.env_vars = self.env_vars;
|
||||
job.signatures = self.signatures;
|
||||
|
||||
Ok(job)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for JobBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,14 @@ pub mod async_runner;
|
||||
pub mod sync_runner;
|
||||
pub mod runner_trait;
|
||||
pub mod script_mode;
|
||||
pub mod job;
|
||||
pub mod client;
|
||||
|
||||
// Public exports for convenience
|
||||
pub use runner_trait::{Runner, RunnerConfig, spawn_runner};
|
||||
pub use async_runner::{AsyncRunner, spawn_async_runner};
|
||||
pub use sync_runner::{SyncRunner, SyncRunnerConfig, spawn_sync_runner};
|
||||
|
||||
// Re-export job types from local job module
|
||||
pub use job::{Job, JobStatus, JobError, JobBuilder};
|
||||
pub use client::{Client, ClientBuilder};
|
||||
// Re-export job types from hero-job crate
|
||||
pub use hero_job::{Job, JobStatus, JobError, JobBuilder, JobSignature, Client, ClientBuilder};
|
||||
pub use redis::AsyncCommands;
|
||||
use log::{error, info};
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
//! └───────────────┘
|
||||
//! ```
|
||||
|
||||
use crate::job::{Job, JobStatus, Client};
|
||||
use crate::{Job, JobStatus, Client};
|
||||
use log::{debug, error, info};
|
||||
use redis::AsyncCommands;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
use crate::job::{JobBuilder, Client, JobStatus};
|
||||
use crate::{JobBuilder, Client, JobStatus};
|
||||
use log::{info, error};
|
||||
use tokio::sync::mpsc;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::job::Job;
|
||||
use crate::Job;
|
||||
use crate::runner_trait::Runner;
|
||||
use log::{debug, error, info};
|
||||
use rhai::{Engine, Dynamic};
|
||||
|
||||
Reference in New Issue
Block a user