Tasks: a renewal created over the API replaces the domain's queued one #308

Merged
jcoffey-dev merged 1 commits from fix/task-renewal-replaces-queued into main 2026-11-07 01:17:37 +00:00
2 changed files with 95 additions and 5 deletions

No files matched your search

+32 -3
View File
@@ -25,7 +25,7 @@ use registry::{
schema::{
enums::{TaskStatusType, TaskType},
prelude::Property,
structs::Task,
structs::{Task, TaskDnsManagement},
},
types::{
EnumImpl, ObjectImpl,
@@ -33,8 +33,8 @@ use registry::{
index::{IndexBuilder, IndexKey},
},
};
use services::task_manager::lock::TaskLockManager;
use std::str::FromStr;
use services::task_manager::{domain_check::clear_queued_renewals, lock::TaskLockManager};
use std::{collections::HashSet, str::FromStr};
use store::{
IterateParams, SerializeInfallible, U64_LEN, ValueKey,
registry::RegistryFilterOp,
@@ -48,6 +48,7 @@ pub(crate) async fn task_set(
) -> trc::Result<RegistrySetResponse<'_>> {
let mut batch = BatchBuilder::new();
let mut locked_tasks = Vec::new();
let mut renewing_domains = HashSet::new();
// Process creations
'outer: for (id, value) in set.create.drain() {
@@ -125,6 +126,34 @@ pub(crate) async fn task_set(
}
}
// inbuxa: a renewal created here replaces the domain's queued
// one, the way saving the domain does, or both would keep
// renewing from here on (#290)
let renews = match &task {
Task::AcmeRenewal(task) => Some(task.domain_id),
Task::DnsManagement(TaskDnsManagement {
domain_id,
on_success_renew_certificate: true,
..
}) => Some(*domain_id),
_ => None,
};
if let Some(domain_id) = renews {
if !renewing_domains.insert(domain_id) {
set.response.not_created.append(
id,
SetError::invalid_properties()
.with_property(Property::DomainId)
.with_description(
"Only one certificate renewal per domain can be created at a time",
),
);
continue 'outer;
}
locked_tasks
.extend(clear_queued_renewals(set.server, domain_id, &mut batch).await?);
}
let task_id = set.server.registry().assign_id();
batch.schedule_task_with_id(task_id, task).commit_point();
set.response.created(id, task_id);
+63 -2
View File
@@ -7,7 +7,7 @@
* Modified by Coffey Labs in 2026 for INBUXA.
*/
use crate::utils::server::TestServer;
use crate::utils::{jmap::JmapUtils, server::TestServer};
use ahash::AHashSet;
use common::network::dns::update::DNS_RECORDS;
use dns_update::DnsRecord;
@@ -21,7 +21,7 @@ use registry::{
CertificateManagementProperties, DkimManagement, DnsManagement,
DnsManagementProperties, DnsServer, DnsServerCloudflare, Domain, PublicText,
PublicTextValue, SecretKey, SecretKeyValue, SecretText, SecretTextValue, Task,
TaskDomainManagement,
TaskDomainManagement, TaskStatus,
},
},
types::{datetime::UTCDateTime, id::ObjectId, map::Map},
@@ -575,6 +575,67 @@ pub async fn test(test: &TestServer) {
test.wait_for_tasks_skip_not_due().await;
assert_one_renewal(account, names_domain_id).await;
// inbuxa: a renewal created over the API replaces the queued one as well,
// and only one can be created per domain in a request
let renewal = || {
Task::AcmeRenewal(TaskDomainManagement {
domain_id: names_domain_id,
status: TaskStatus::now(),
})
};
account.registry_create_object(renewal()).await;
assert_one_renewal(account, names_domain_id).await;
test.wait_for_tasks_skip_not_due().await;
assert_one_renewal(account, names_domain_id).await;
let response = account.registry_create([renewal(), renewal()]).await;
response.created_id(0);
response
.not_created(1)
.to_set_error()
.assert_type(SetErrorType::InvalidProperties)
.assert_description_contains("Only one certificate renewal per domain");
test.wait_for_tasks_skip_not_due().await;
assert_one_renewal(account, names_domain_id).await;
// A queued renewal can be told to run now, as the console's Order again
// does. The certificate is still valid, so it only reschedules itself.
let (renewal_id, queued) = account
.registry_get_all::<Task>()
.await
.into_iter()
.find(|(_, task)| {
matches!(task, Task::AcmeRenewal(TaskDomainManagement { domain_id, .. }) if *domain_id == names_domain_id)
})
.unwrap();
let due_before = queued.due_timestamp();
let now_text = UTCDateTime::now().to_string();
account
.registry_update_object(
ObjectType::Task,
renewal_id,
json!({ "status": { "@type": "Pending", "due": now_text } }),
)
.await;
test.wait_for_tasks_skip_not_due().await;
assert_one_renewal(account, names_domain_id).await;
let due_after = account
.registry_get_all::<Task>()
.await
.into_iter()
.find_map(|(_, task)| match task {
Task::AcmeRenewal(TaskDomainManagement { domain_id, .. })
if domain_id == names_domain_id =>
{
Some(task.due_timestamp())
}
_ => None,
})
.unwrap();
assert_eq!(
due_after, due_before,
"the run now should reschedule for when the certificate falls due"
);
// DNS-PERSIST-01 only allows a wildcard if its record says so. Going from
// named hosts to the wildcard needs that record republished first.
let names_persist_id = account