use super::{
map_write_routine_err, max_runtime_ceiling_secs, migrate_workbenches, min_schedule_ceiling,
normalize_model, now_secs, reject_blank, reject_over_ceiling, reject_zero_secs,
remove_routine_dir, slugify, ttl_ceiling_secs, validate_agent,
validate_and_normalize_schedules, validate_env, validate_goal, validate_machines,
validate_prompt, validate_repositories, validate_tags, validate_title, write_routine, AppError,
LockRecover, RoutineResponse, RoutineStore, UpdateRoutineRequest,
};
pub fn svc_update(
store: &RoutineStore,
id: &str,
req: UpdateRoutineRequest,
) -> Result<RoutineResponse, AppError> {
if req.schedule.is_some() && req.schedules.is_some() {
return Err(AppError::BadRequest(
"send either schedule or schedules, not both".to_string(),
));
}
let requested_schedules = match (&req.schedule, &req.schedules) {
(Some(schedule), None) => Some(validate_and_normalize_schedules(std::slice::from_ref(
schedule,
))?),
(None, Some(schedules)) => Some(validate_and_normalize_schedules(schedules)?),
(None, None) | (Some(_), Some(_)) => None,
};
if let Some(ref title) = req.title {
reject_blank("title", title)?;
validate_title(title)?;
}
if let Some(ref prompt) = req.prompt {
validate_prompt(prompt)?;
}
if let Some(ref agent) = req.agent {
validate_agent(agent)?;
}
reject_zero_secs("ttl_secs", req.ttl_secs)?;
reject_zero_secs("max_runtime_secs", req.max_runtime_secs)?;
let repositories = match req.repositories {
Some(ref repos) => Some(validate_repositories(repos)?),
None => None,
};
let tags = match req.tags {
Some(ref tags) => Some(validate_tags(tags)?),
None => None,
};
let goal = match req.goal {
Some(ref goal) => Some(validate_goal(Some(goal))?),
None => None,
};
let machines = match req.machines {
Some(ref machines) => Some(validate_machines(machines)?),
None => None,
};
if let Some(ref env) = req.env {
validate_env(env)?;
}
let mut lock = store.lock_recover();
let old_slug = slugify(&lock.get(id).ok_or(AppError::NotFound)?.title);
if let Some(ref new_title) = req.title {
let new_slug = slugify(new_title);
if new_slug != old_slug
&& lock
.values()
.any(|routine| routine.id != id && slugify(&routine.title) == new_slug)
{
return Err(AppError::Conflict(format!(
"a routine with the name \"{new_slug}\" already exists"
)));
}
}
let effective_schedules = if let Some(schedules) = &requested_schedules {
schedules.clone()
} else {
#[allow(
clippy::expect_used,
reason = "id existence was checked via `ok_or(AppError::NotFound)` above, and \
`lock` has been held continuously since — a miss here is a logic bug, \
not a recoverable runtime error"
)]
let routine = lock
.get(id)
.expect("id existence checked above, and the lock has been held continuously since");
routine.effective_schedules()
};
reject_over_ceiling(
"ttl_secs",
req.ttl_secs,
min_schedule_ceiling(&effective_schedules, ttl_ceiling_secs),
)?;
reject_over_ceiling(
"max_runtime_secs",
req.max_runtime_secs,
min_schedule_ceiling(&effective_schedules, max_runtime_ceiling_secs),
)?;
#[allow(
clippy::expect_used,
reason = "id existence was checked via `ok_or(AppError::NotFound)` above, and `lock` has \
been held continuously since — a miss here is a logic bug, not a recoverable \
runtime error"
)]
let routine = lock
.get_mut(id)
.expect("id existence checked above, and the lock has been held continuously since");
if let Some(schedules) = requested_schedules {
routine.schedule = schedules[0].clone();
routine.schedules = schedules;
}
if let Some(title) = req.title {
routine.title = title.trim().to_string();
}
if let Some(agent) = req.agent {
routine.agent = agent;
}
if let Some(model) = req.model {
routine.model = normalize_model(Some(model));
}
if let Some(prompt) = req.prompt {
routine.prompt = prompt;
}
if let Some(goal) = goal {
routine.goal = goal;
}
if let Some(repositories) = repositories {
routine.repositories = repositories;
}
if let Some(machines) = machines {
routine.machines = machines;
}
if let Some(enabled) = req.enabled {
routine.enabled = enabled;
}
if req.enabled == Some(true) {
routine.consecutive_failures = 0;
routine.auto_disabled_reason = None;
}
if let Some(ttl) = req.ttl_secs {
routine.ttl_secs = Some(ttl);
}
if let Some(max_runtime) = req.max_runtime_secs {
routine.max_runtime_secs = Some(max_runtime);
}
if let Some(threshold) = req.failure_threshold {
routine.failure_threshold = Some(threshold);
}
if let Some(tags) = tags {
routine.tags = tags;
}
if let Some(env) = req.env {
routine.env = env;
}
routine.updated_at = now_secs();
let routine = routine.clone();
drop(lock);
let new_slug = slugify(&routine.title);
write_routine(&routine).map_err(|err| map_write_routine_err(&err))?;
if new_slug != old_slug {
migrate_workbenches(&old_slug, &new_slug);
remove_routine_dir(&old_slug).map_err(|_| AppError::Internal)?;
}
if let Err(err) = crate::sync::routines::sync_routines_to_crontab(store) {
log::warn!("crontab sync after routine update failed: {err}");
}
Ok(RoutineResponse::from_routine(routine))
}