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

@@ -19,7 +19,6 @@ use sal_service_manager::{ProcessStatus, LogInfo};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use std::fs;
use tokio::sync::Mutex;
@@ -84,6 +83,31 @@ pub struct RunJobParams {
pub job: Job,
}
/// Request parameters for starting a job
#[derive(Debug, Deserialize, Serialize)]
pub struct StartJobParams {
pub secret: String,
pub job_id: String,
}
/// Job result response
#[derive(Debug, Serialize, Clone)]
#[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>,
}
/// Request parameters for adding a new runner
#[derive(Debug, Deserialize, Serialize)]
pub struct AddRunnerParams {
@@ -98,18 +122,32 @@ pub struct AddRunnerParams {
/// Request parameters for queuing a job
#[derive(Debug, Deserialize, Serialize)]
pub struct QueueJobParams {
pub runner_name: String,
pub runner: String,
pub job: Job,
}
/// Request parameters for queue and wait operation
#[derive(Debug, Deserialize, Serialize)]
pub struct QueueAndWaitParams {
pub runner_name: String,
pub runner: String,
pub job: Job,
pub timeout_secs: u64,
}
/// Request parameters for stopping a job
#[derive(Debug, Deserialize, Serialize)]
pub struct StopJobParams {
pub secret: String,
pub job_id: String,
}
/// Request parameters for deleting a job
#[derive(Debug, Deserialize, Serialize)]
pub struct DeleteJobParams {
pub secret: String,
pub job_id: String,
}
/// Request parameters for getting runner logs
#[derive(Debug, Deserialize, Serialize)]
pub struct GetLogsParams {
@@ -236,13 +274,37 @@ pub trait SupervisorRpc {
#[method(name = "register_runner")]
async fn register_runner(&self, params: RegisterRunnerParams) -> RpcResult<String>;
/// Create a job (fire-and-forget, non-blocking)
#[method(name = "create_job")]
async fn create_job(&self, params: RunJobParams) -> RpcResult<String>;
/// Create a job without queuing it to a runner
#[method(name = "jobs.create")]
async fn jobs_create(&self, params: RunJobParams) -> RpcResult<String>;
/// Run a job on the appropriate runner (blocking, returns result)
#[method(name = "run_job")]
async fn run_job(&self, params: RunJobParams) -> RpcResult<Option<String>>;
/// List all jobs
#[method(name = "jobs.list")]
async fn jobs_list(&self) -> RpcResult<Vec<Job>>;
/// Run a job on the appropriate runner and return the result
#[method(name = "job.run")]
async fn job_run(&self, params: RunJobParams) -> RpcResult<JobResult>;
/// Start a previously created job by queuing it to its assigned runner
#[method(name = "job.start")]
async fn job_start(&self, params: StartJobParams) -> RpcResult<()>;
/// Get the current status of a job
#[method(name = "job.status")]
async fn job_status(&self, job_id: String) -> RpcResult<JobStatusResponse>;
/// Get the result of a completed job (blocks until result is available)
#[method(name = "job.result")]
async fn job_result(&self, job_id: String) -> RpcResult<JobResult>;
/// Stop a running job
#[method(name = "job.stop")]
async fn job_stop(&self, params: StopJobParams) -> RpcResult<()>;
/// Delete a job from the system
#[method(name = "job.delete")]
async fn job_delete(&self, params: DeleteJobParams) -> RpcResult<()>;
/// Remove a runner from the supervisor
#[method(name = "remove_runner")]
@@ -276,9 +338,6 @@ pub trait SupervisorRpc {
#[method(name = "queue_job_to_runner")]
async fn queue_job_to_runner(&self, params: QueueJobParams) -> RpcResult<()>;
/// List all job IDs from Redis
#[method(name = "list_jobs")]
async fn list_jobs(&self) -> RpcResult<Vec<String>>;
/// Get a job by job ID
#[method(name = "get_job")]
@@ -381,8 +440,8 @@ impl SupervisorRpcServer for Arc<Mutex<Supervisor>> {
Ok(params.name)
}
async fn create_job(&self, params: RunJobParams) -> RpcResult<String> {
debug!("OpenRPC request: create_job with params: {:?}", params);
async fn jobs_create(&self, params: RunJobParams) -> RpcResult<String> {
debug!("OpenRPC request: jobs.create with params: {:?}", params);
let mut supervisor = self.lock().await;
let job_id = supervisor
@@ -393,12 +452,85 @@ impl SupervisorRpcServer for Arc<Mutex<Supervisor>> {
Ok(job_id)
}
async fn run_job(&self, params: RunJobParams) -> RpcResult<Option<String>> {
debug!("OpenRPC request: run_job with params: {:?}", params);
async fn jobs_list(&self) -> RpcResult<Vec<Job>> {
debug!("OpenRPC request: jobs.list");
let supervisor = self.lock().await;
supervisor
.list_all_jobs()
.await
.map_err(runner_error_to_rpc_error)
}
async fn job_run(&self, params: RunJobParams) -> RpcResult<JobResult> {
debug!("OpenRPC request: job.run with params: {:?}", params);
let mut supervisor = self.lock().await;
match supervisor
.run_job(&params.secret, params.job)
.await
.map_err(runner_error_to_rpc_error)? {
Some(output) => Ok(JobResult::Success { success: output }),
None => Ok(JobResult::Error { error: "Job execution failed".to_string() })
}
}
async fn job_start(&self, params: StartJobParams) -> RpcResult<()> {
debug!("OpenRPC request: job.start with params: {:?}", params);
let mut supervisor = self.lock().await;
supervisor
.run_job(&params.secret, params.job)
.start_job(&params.secret, &params.job_id)
.await
.map_err(runner_error_to_rpc_error)
}
async fn job_status(&self, job_id: String) -> RpcResult<JobStatusResponse> {
debug!("OpenRPC request: job.status with job_id: {}", job_id);
let supervisor = self.lock().await;
let status = supervisor
.get_job_status(&job_id)
.await
.map_err(runner_error_to_rpc_error)?;
Ok(status)
}
async fn job_result(&self, job_id: String) -> RpcResult<JobResult> {
debug!("OpenRPC request: job.result with job_id: {}", job_id);
let supervisor = self.lock().await;
match supervisor
.get_job_result(&job_id)
.await
.map_err(runner_error_to_rpc_error)? {
Some(result) => {
if result.starts_with("Error:") {
Ok(JobResult::Error { error: result })
} else {
Ok(JobResult::Success { success: result })
}
},
None => Ok(JobResult::Error { error: "Job result not available".to_string() })
}
}
async fn job_stop(&self, params: StopJobParams) -> RpcResult<()> {
debug!("OpenRPC request: job.stop with params: {:?}", params);
let mut supervisor = self.lock().await;
supervisor
.stop_job(&params.job_id)
.await
.map_err(runner_error_to_rpc_error)
}
async fn job_delete(&self, params: DeleteJobParams) -> RpcResult<()> {
debug!("OpenRPC request: job.delete with params: {:?}", params);
let mut supervisor = self.lock().await;
supervisor
.delete_job(&params.job_id)
.await
.map_err(runner_error_to_rpc_error)
}
@@ -469,19 +601,11 @@ impl SupervisorRpcServer for Arc<Mutex<Supervisor>> {
debug!("OpenRPC request: queue_job_to_runner with params: {:?}", params);
let mut supervisor = self.lock().await;
supervisor
.queue_job_to_runner(&params.runner_name, params.job)
.queue_job_to_runner(&params.runner, params.job)
.await
.map_err(runner_error_to_rpc_error)
}
async fn list_jobs(&self) -> RpcResult<Vec<String>> {
debug!("OpenRPC request: list_jobs");
let supervisor = self.lock().await;
supervisor
.list_jobs()
.await
.map_err(runner_error_to_rpc_error)
}
async fn get_job(&self, job_id: String) -> RpcResult<Job> {
debug!("OpenRPC request: get_job with job_id: {}", job_id);
@@ -523,7 +647,7 @@ impl SupervisorRpcServer for Arc<Mutex<Supervisor>> {
debug!("OpenRPC request: queue_and_wait with params: {:?}", params);
let mut supervisor = self.lock().await;
supervisor
.queue_and_wait(&params.runner_name, params.job, params.timeout_secs)
.queue_and_wait(&params.runner, params.job, params.timeout_secs)
.await
.map_err(runner_error_to_rpc_error)
}
@@ -810,20 +934,108 @@ pub async fn start_openrpc_servers(
#[cfg(test)]
mod tests {
use super::*;
use crate::supervisor::Supervisor;
#[test]
fn test_supervisor_rpc_creation() {
let _rpc = SupervisorRpcImpl::new();
// Just test that we can create the RPC implementation
#[tokio::test]
async fn test_supervisor_rpc_creation() {
// Test that we can create a supervisor and use it with RPC
use crate::supervisor::SupervisorBuilder;
let supervisor = SupervisorBuilder::new()
.redis_url("redis://localhost:6379")
.namespace("test")
.build()
.await;
// Just test that we can build a supervisor
assert!(supervisor.is_ok() || supervisor.is_err()); // Either way is fine for this test
}
#[cfg(feature = "openrpc")]
#[test]
fn test_process_manager_type_parsing() {
assert!(SupervisorRpcImpl::parse_process_manager_type("simple").is_ok());
assert!(SupervisorRpcImpl::parse_process_manager_type("tmux").is_ok());
assert!(SupervisorRpcImpl::parse_process_manager_type("Simple").is_ok());
assert!(SupervisorRpcImpl::parse_process_manager_type("TMUX").is_ok());
assert!(SupervisorRpcImpl::parse_process_manager_type("invalid").is_err());
assert!(parse_process_manager_type("simple", None).is_ok());
assert!(parse_process_manager_type("tmux", Some("session".to_string())).is_ok());
assert!(parse_process_manager_type("Simple", None).is_ok());
assert!(parse_process_manager_type("TMUX", Some("session".to_string())).is_ok());
assert!(parse_process_manager_type("invalid", None).is_err());
}
#[tokio::test]
async fn test_job_api_methods() {
let supervisor = Arc::new(Mutex::new(Supervisor::default()));
let mut sup = supervisor.lock().await;
sup.add_user_secret("test-secret".to_string());
drop(sup);
// Test jobs.create
let job = crate::job::JobBuilder::new()
.caller_id("test")
.context_id("test")
.payload("test")
.runner("test_runner")
.executor("osis")
.build()
.unwrap();
let params = RunJobParams {
secret: "test-secret".to_string(),
job: job.clone(),
};
let result = supervisor.jobs_create(params).await;
// Should work or fail gracefully without Redis
assert!(result.is_ok() || result.is_err());
// Test job.start
let start_params = StartJobParams {
secret: "test-secret".to_string(),
job_id: "test-job".to_string(),
};
let result = supervisor.job_start(start_params).await;
// Should fail gracefully without Redis/job
assert!(result.is_err());
// Test invalid secret
let invalid_params = StartJobParams {
secret: "invalid".to_string(),
job_id: "test-job".to_string(),
};
let result = supervisor.job_start(invalid_params).await;
assert!(result.is_err());
}
#[test]
fn test_job_result_serialization() {
let success = JobResult::Success { success: "test output".to_string() };
let json = serde_json::to_string(&success).unwrap();
assert!(json.contains("success"));
assert!(json.contains("test output"));
let error = JobResult::Error { error: "test error".to_string() };
let json = serde_json::to_string(&error).unwrap();
assert!(json.contains("error"));
assert!(json.contains("test error"));
}
#[test]
fn test_job_status_response_serialization() {
let status = JobStatusResponse {
job_id: "test-job".to_string(),
status: "running".to_string(),
created_at: "2023-01-01T00:00:00Z".to_string(),
started_at: Some("2023-01-01T00:00:05Z".to_string()),
completed_at: None,
};
let json = serde_json::to_string(&status).unwrap();
assert!(json.contains("test-job"));
assert!(json.contains("running"));
assert!(json.contains("2023-01-01T00:00:00Z"));
let deserialized: JobStatusResponse = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.job_id, "test-job");
assert_eq!(deserialized.status, "running");
}
}