update api, fix tests and examples

This commit is contained in:
Timur Gordon
2025-08-27 10:07:53 +02:00
parent 767c66fb6a
commit ef17d36300
42 changed files with 2984 additions and 781 deletions

View File

@@ -28,7 +28,6 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
use thiserror::Error;
use serde_json;
use uuid::Uuid;
@@ -157,32 +156,34 @@ pub struct RunnerConfig {
pub redis_url: String,
}
/// Job type enumeration that maps to runner types
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum JobType {
/// SAL job type
SAL,
/// OSIS job type
OSIS,
/// V job type
V,
/// Python job type
Python,
}
/// Job status enumeration
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum JobStatus {
/// Job has been created but not yet dispatched
Created,
/// Job has been dispatched to a worker queue
Dispatched,
/// Job is currently being executed
WaitingForPrerequisites,
Started,
/// Job completed successfully
Finished,
/// Job completed with an error
Error,
Stopping,
Finished,
}
/// Job result response
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum JobResult {
Success { success: String },
Error { error: String },
}
/// Job status response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobStatusResponse {
pub job_id: String,
pub status: String,
pub created_at: String,
pub started_at: Option<String>,
pub completed_at: Option<String>,
}
/// Job structure for creating and managing jobs
@@ -196,20 +197,18 @@ pub struct Job {
pub context_id: String,
/// Script content or payload to execute
pub payload: String,
/// Type of job (determines which actor will process it)
pub job_type: JobType,
/// Name of the specific runner/actor to execute this job
pub runner_name: String,
/// Current status of the job
pub status: JobStatus,
pub runner: String,
/// Name of the executor the runner will use to execute this job
pub executor: String,
/// Job execution timeout (in seconds)
pub timeout: u64,
/// Environment variables for job execution
pub env_vars: HashMap<String, String>,
/// Timestamp when the job was created
pub created_at: String,
/// Timestamp when the job was last updated
pub updated_at: String,
/// Job execution timeout
pub timeout: Duration,
/// Environment variables for job execution
pub env_vars: HashMap<String, String>,
}
/// Process status wrapper for OpenRPC serialization (matches server response)
@@ -257,7 +256,7 @@ impl SupervisorClient {
let server_url = server_url.into();
let client = HttpClientBuilder::default()
.request_timeout(Duration::from_secs(30))
.request_timeout(std::time::Duration::from_secs(30))
.build(&server_url)
.map_err(|e| ClientError::Http(e.to_string()))?;
@@ -299,15 +298,83 @@ impl SupervisorClient {
Ok(())
}
/// Run a job on the appropriate runner
pub async fn run_job(
/// Create a new job without queuing it to a runner
pub async fn jobs_create(
&self,
secret: &str,
job: serde_json::Value,
) -> ClientResult<Option<String>> {
let result: Option<String> = self
job: Job,
) -> ClientResult<String> {
let params = serde_json::json!({
"secret": secret,
"job": job
});
let job_id: String = self
.client
.request("run_job", rpc_params![secret, job])
.request("jobs.create", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(job_id)
}
/// List all jobs
pub async fn jobs_list(&self) -> ClientResult<Vec<Job>> {
let jobs: Vec<Job> = self
.client
.request("jobs.list", rpc_params![])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(jobs)
}
/// Run a job on the appropriate runner and return the result
pub async fn job_run(
&self,
secret: &str,
job: Job,
) -> ClientResult<JobResult> {
let params = serde_json::json!({
"secret": secret,
"job": job
});
let result: JobResult = self
.client
.request("job.run", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(result)
}
/// Start a previously created job by queuing it to its assigned runner
pub async fn job_start(
&self,
secret: &str,
job_id: &str,
) -> ClientResult<()> {
let params = serde_json::json!({
"secret": secret,
"job_id": job_id
});
let _: () = self
.client
.request("job.start", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// Get the current status of a job
pub async fn job_status(&self, job_id: &str) -> ClientResult<JobStatusResponse> {
let status: JobStatusResponse = self
.client
.request("job.status", rpc_params![job_id])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(status)
}
/// Get the result of a completed job (blocks until result is available)
pub async fn job_result(&self, job_id: &str) -> ClientResult<JobResult> {
let result: JobResult = self
.client
.request("job.result", rpc_params![job_id])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(result)
}
@@ -347,6 +414,15 @@ impl SupervisorClient {
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// Add a runner to the supervisor
pub async fn add_runner(&self, config: RunnerConfig, process_manager: ProcessManagerType) -> ClientResult<()> {
let _: () = self
.client
.request("add_runner", rpc_params![config, process_manager])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// Get status of a specific runner
pub async fn get_runner_status(&self, actor_id: &str) -> ClientResult<ProcessStatus> {
@@ -408,9 +484,9 @@ impl SupervisorClient {
}
/// Queue a job to a specific runner
pub async fn queue_job_to_runner(&self, runner_name: &str, job: Job) -> ClientResult<()> {
pub async fn queue_job_to_runner(&self, runner: &str, job: Job) -> ClientResult<()> {
let params = serde_json::json!({
"runner_name": runner_name,
"runner": runner,
"job": job
});
@@ -423,9 +499,9 @@ impl SupervisorClient {
/// Queue a job to a specific runner and wait for the result
/// This implements the proper Hero job protocol with BLPOP on reply queue
pub async fn queue_and_wait(&self, runner_name: &str, job: Job, timeout_secs: u64) -> ClientResult<Option<String>> {
pub async fn queue_and_wait(&self, runner: &str, job: Job, timeout_secs: u64) -> ClientResult<Option<String>> {
let params = serde_json::json!({
"runner_name": runner_name,
"runner": runner,
"job": job,
"timeout_secs": timeout_secs
});
@@ -573,6 +649,30 @@ impl SupervisorClient {
Ok(info)
}
/// Stop a running job
pub async fn job_stop(&self, secret: &str, job_id: &str) -> ClientResult<()> {
let params = serde_json::json!({
"secret": secret,
"job_id": job_id
});
self.client
.request("job.stop", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))
}
/// Delete a job from the system
pub async fn job_delete(&self, secret: &str, job_id: &str) -> ClientResult<()> {
let params = serde_json::json!({
"secret": secret,
"job_id": job_id
});
self.client
.request("job.delete", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))
}
/// Get supervisor information including secret counts
pub async fn get_supervisor_info(&self, admin_secret: &str) -> ClientResult<SupervisorInfo> {
let info: SupervisorInfo = self
@@ -588,9 +688,9 @@ pub struct JobBuilder {
caller_id: String,
context_id: String,
payload: String,
job_type: JobType,
runner_name: String,
timeout: Duration,
runner: String,
executor: String,
timeout: u64, // timeout in seconds
env_vars: HashMap<String, String>,
}
@@ -601,9 +701,9 @@ impl JobBuilder {
caller_id: "".to_string(),
context_id: "".to_string(),
payload: "".to_string(),
job_type: JobType::SAL, // default
runner_name: "".to_string(),
timeout: Duration::from_secs(300), // 5 minutes default
runner: "".to_string(),
executor: "".to_string(),
timeout: 300, // 5 minutes default
env_vars: HashMap::new(),
}
}
@@ -626,20 +726,20 @@ impl JobBuilder {
self
}
/// Set the job type
pub fn job_type(mut self, job_type: JobType) -> Self {
self.job_type = job_type;
/// Set the executor for this job
pub fn executor(mut self, executor: impl Into<String>) -> Self {
self.executor = executor.into();
self
}
/// Set the runner name for this job
pub fn runner_name(mut self, runner_name: impl Into<String>) -> Self {
self.runner_name = runner_name.into();
pub fn runner(mut self, runner: impl Into<String>) -> Self {
self.runner = runner.into();
self
}
/// Set the timeout for job execution
pub fn timeout(mut self, timeout: Duration) -> Self {
/// Set the timeout for job execution (in seconds)
pub fn timeout(mut self, timeout: u64) -> Self {
self.timeout = timeout;
self
}
@@ -673,9 +773,14 @@ impl JobBuilder {
message: "payload is required".to_string(),
});
}
if self.runner_name.is_empty() {
if self.runner.is_empty() {
return Err(ClientError::Server {
message: "runner_name is required".to_string(),
message: "runner is required".to_string(),
});
}
if self.executor.is_empty() {
return Err(ClientError::Server {
message: "executor is required".to_string(),
});
}
@@ -686,13 +791,12 @@ impl JobBuilder {
caller_id: self.caller_id,
context_id: self.context_id,
payload: self.payload,
job_type: self.job_type,
runner_name: self.runner_name,
status: JobStatus::Created,
created_at: now.clone(),
updated_at: now,
runner: self.runner,
executor: self.executor,
timeout: self.timeout,
env_vars: self.env_vars,
created_at: now.clone(),
updated_at: now,
})
}
}
@@ -722,9 +826,9 @@ mod tests {
.caller_id("test_client")
.context_id("test_context")
.payload("print('Hello, World!');")
.job_type(JobType::OSIS)
.runner_name("test_runner")
.timeout(Duration::from_secs(60))
.executor("osis")
.runner("test_runner")
.timeout(60)
.env_var("TEST_VAR", "test_value")
.build();
@@ -734,11 +838,10 @@ mod tests {
assert_eq!(job.caller_id, "test_client");
assert_eq!(job.context_id, "test_context");
assert_eq!(job.payload, "print('Hello, World!');");
assert_eq!(job.job_type, JobType::OSIS);
assert_eq!(job.runner_name, "test_runner");
assert_eq!(job.timeout, Duration::from_secs(60));
assert_eq!(job.executor, "osis");
assert_eq!(job.runner, "test_runner");
assert_eq!(job.timeout, 60);
assert_eq!(job.env_vars.get("TEST_VAR"), Some(&"test_value".to_string()));
assert_eq!(job.status, JobStatus::Created);
}
#[test]
@@ -747,7 +850,7 @@ mod tests {
let result = JobBuilder::new()
.context_id("test")
.payload("test")
.runner_name("test")
.runner("test")
.build();
assert!(result.is_err());
@@ -755,7 +858,7 @@ mod tests {
let result = JobBuilder::new()
.caller_id("test")
.payload("test")
.runner_name("test")
.runner("test")
.build();
assert!(result.is_err());
@@ -763,15 +866,26 @@ mod tests {
let result = JobBuilder::new()
.caller_id("test")
.context_id("test")
.runner_name("test")
.runner("test")
.executor("test")
.build();
assert!(result.is_err());
// Missing runner_name
// Missing runner
let result = JobBuilder::new()
.caller_id("test")
.context_id("test")
.payload("test")
.executor("test")
.build();
assert!(result.is_err());
// Missing executor
let result = JobBuilder::new()
.caller_id("test")
.context_id("test")
.payload("test")
.runner("test")
.build();
assert!(result.is_err());
}
@@ -885,7 +999,7 @@ mod client_tests {
assert_eq!(job.id(), "test-id");
assert_eq!(job.payload(), "test payload");
assert_eq!(job.job_type(), "SAL");
assert_eq!(job.runner_name(), "test-runner");
assert_eq!(job.runner(), "test-runner");
assert_eq!(job.caller_id(), "wasm_client");
assert_eq!(job.context_id(), "wasm_context");
assert_eq!(job.timeout_secs(), 30);
@@ -940,7 +1054,7 @@ mod client_tests {
assert_eq!(job.id(), "func-test-id");
assert_eq!(job.payload(), "func test payload");
assert_eq!(job.job_type(), "OSIS");
assert_eq!(job.runner_name(), "func-test-runner");
assert_eq!(job.runner(), "func-test-runner");
}
#[wasm_bindgen_test]