Files
source-gamepanel/apps/daemon/src/scheduler/mod.rs
T
2026-08-02 20:26:54 +03:00

168 lines
5.3 KiB
Rust

use std::sync::Arc;
use anyhow::Result;
use tokio::time::{interval, Duration};
use tracing::{info, error, warn};
use serde::Deserialize;
use crate::command::CommandDispatcher;
use crate::server::ServerManager;
/// A scheduled task received from the panel API.
#[derive(Debug, Clone, Deserialize)]
pub struct ScheduledTask {
pub id: String,
pub server_uuid: String,
pub action: String, // "command", "power", "backup"
pub payload: String, // command string, power action, or "backup"
pub schedule_type: String,
pub is_active: bool,
pub next_run_at: Option<String>, // ISO 8601
}
/// Scheduler that polls the panel API for due tasks and executes them.
pub struct Scheduler {
server_manager: Arc<ServerManager>,
command_dispatcher: Arc<CommandDispatcher>,
api_url: String,
node_token: String,
poll_interval_secs: u64,
}
impl Scheduler {
pub fn new(
server_manager: Arc<ServerManager>,
command_dispatcher: Arc<CommandDispatcher>,
api_url: String,
node_token: String,
) -> Self {
Self {
server_manager,
command_dispatcher,
api_url,
node_token,
poll_interval_secs: 15,
}
}
/// Run the scheduler loop. This should be spawned as a tokio task.
pub async fn run(self: Arc<Self>) {
info!("Scheduler started (poll interval: {}s)", self.poll_interval_secs);
let mut tick = interval(Duration::from_secs(self.poll_interval_secs));
loop {
tick.tick().await;
if let Err(e) = self.poll_and_execute().await {
error!(error = %e, "Scheduler poll failed");
}
}
}
/// Poll the API for due tasks and execute them.
async fn poll_and_execute(&self) -> Result<()> {
let client = reqwest::Client::new();
let url = format!("{}/api/internal/schedules/due", self.api_url);
let resp = client
.get(&url)
.bearer_auth(&self.node_token)
.send()
.await?;
if !resp.status().is_success() {
warn!(status = %resp.status(), "Failed to fetch due tasks");
return Ok(());
}
#[derive(Deserialize)]
struct DueResponse {
tasks: Vec<ScheduledTask>,
}
let due: DueResponse = resp.json().await?;
if due.tasks.is_empty() {
return Ok(());
}
info!(count = due.tasks.len(), "Processing due scheduled tasks");
for task in &due.tasks {
if let Err(e) = self.execute_task(task).await {
error!(
task_id = %task.id,
server = %task.server_uuid,
error = %e,
"Failed to execute scheduled task"
);
}
// Notify API that task was executed
let ack_url = format!(
"{}/api/internal/schedules/{}/ack",
self.api_url, task.id
);
let _ = client
.post(&ack_url)
.bearer_auth(&self.node_token)
.send()
.await;
}
Ok(())
}
/// Execute a single scheduled task.
async fn execute_task(&self, task: &ScheduledTask) -> Result<()> {
info!(
task_id = %task.id,
action = %task.action,
server = %task.server_uuid,
"Executing scheduled task"
);
match task.action.as_str() {
"command" => {
self.command_dispatcher
.send_command(&task.server_uuid, &task.payload)
.await?;
}
"power" => {
match task.payload.as_str() {
"start" => self.server_manager.start_server(&task.server_uuid).await?,
"stop" => self.server_manager.stop_server(&task.server_uuid, None, 0).await?,
"restart" => {
let _ = self.server_manager.stop_server(&task.server_uuid, None, 0).await;
tokio::time::sleep(Duration::from_secs(3)).await;
self.server_manager.start_server(&task.server_uuid).await?;
}
"kill" => self.server_manager.kill_server(&task.server_uuid).await?,
_ => warn!(payload = %task.payload, "Unknown power action"),
}
}
"backup" => {
// Trigger backup via the backup module
info!(
server = %task.server_uuid,
"Backup scheduled task — delegating to backup module"
);
// Backup is handled by sending callback to API
let client = reqwest::Client::new();
let url = format!(
"{}/api/internal/servers/{}/backup",
self.api_url, task.server_uuid
);
let _ = client
.post(&url)
.bearer_auth(&self.node_token)
.json(&serde_json::json!({ "name": format!("auto-{}", task.id) }))
.send()
.await;
}
_ => {
warn!(action = %task.action, "Unknown scheduled action");
}
}
Ok(())
}
}