Registry writes apply to the running settings without ReloadSettings #39

Merged
jcoffey-dev merged 1 commits from fix/registry-auto-reload into main 2026-09-24 19:34:08 +00:00
10 changed files with 520 additions and 68 deletions
Showing only changes of commit 2c684be5c9 - Show all commits
+199 -1
View File
@@ -13,7 +13,7 @@ use crate::{
storage::Storage, storage::Storage,
telemetry::Telemetry, telemetry::Telemetry,
}, },
ipc::{QueueEvent, RegistryChange}, ipc::{BroadcastEvent, QueueEvent, RegistryChange},
network::security::{BlockedIps, IpWithTtl}, network::security::{BlockedIps, IpWithTtl},
}; };
use ahash::AHashMap; use ahash::AHashMap;
@@ -232,3 +232,201 @@ fn error_object(error: &Error) -> Option<ObjectId> {
Error::Internal { object_id, .. } => *object_id, Error::Internal { object_id, .. } => *object_id,
} }
} }
// inbuxa: upstream applied a registry write to the running settings only on
// an explicit x:Action ReloadSettings (Directory and Authentication aside), so
// a new MtaDeliverySchedule, say, stayed unknown ("Queue strategy not found")
// until someone reloaded. Writes to objects the settings are built from now
// reload them, here and across the cluster, as ReloadSettings does.
/// Coalesces the full reloads that registry writes trigger: a write waits for
/// a reload that started after it was stored, and joins one if it can, so a
/// burst of writes costs a reload or two rather than one each.
#[derive(Default)]
pub struct SettingsReloadGate {
requested: std::sync::atomic::AtomicU64,
state: tokio::sync::Mutex<SettingsReloadState>,
}
#[derive(Default)]
struct SettingsReloadState {
completed: u64,
refused: Option<String>,
}
/// The reload a write to `object` calls for: the object to reload, or None
/// when the running settings don't hold that object (accounts, domains and
/// other data read as needed, stores, which take a restart, and objects with
/// reload actions of their own, such as applications).
pub fn write_reload_target(object: ObjectType) -> Option<ObjectType> {
match object {
ObjectType::Certificate => Some(ObjectType::Certificate),
ObjectType::MemoryLookupKey
| ObjectType::MemoryLookupKeyValue
| ObjectType::HttpLookup
| ObjectType::StoreLookup => Some(ObjectType::StoreLookup),
ObjectType::BlockedIp | ObjectType::AllowedIp => Some(ObjectType::BlockedIp),
ObjectType::AcmeProvider
| ObjectType::AddressBook
| ObjectType::AiModel
| ObjectType::Asn
| ObjectType::Authentication
| ObjectType::Cache
| ObjectType::Calendar
| ObjectType::CalendarAlarm
| ObjectType::CalendarScheduling
| ObjectType::ClusterRole
| ObjectType::DataRetention
| ObjectType::Directory
| ObjectType::DkimReportSettings
| ObjectType::DmarcReportSettings
| ObjectType::DnsResolver
| ObjectType::DsnReportSettings
| ObjectType::Email
| ObjectType::EventTracingLevel
| ObjectType::FileStorage
| ObjectType::Http
| ObjectType::HttpForm
| ObjectType::Imap
| ObjectType::Jmap
| ObjectType::Metrics
| ObjectType::MtaConnectionStrategy
| ObjectType::MtaDeliverySchedule
| ObjectType::MtaExtensions
| ObjectType::MtaHook
| ObjectType::MtaInboundSession
| ObjectType::MtaInboundThrottle
| ObjectType::MtaMilter
| ObjectType::MtaOutboundStrategy
| ObjectType::MtaOutboundThrottle
| ObjectType::MtaQueueQuota
| ObjectType::MtaRoute
| ObjectType::MtaStageAuth
| ObjectType::MtaStageConnect
| ObjectType::MtaStageData
| ObjectType::MtaStageEhlo
| ObjectType::MtaStageMail
| ObjectType::MtaStageRcpt
| ObjectType::MtaSts
| ObjectType::MtaTlsStrategy
| ObjectType::MtaVirtualQueue
| ObjectType::NetworkListener
| ObjectType::OidcProvider
| ObjectType::ReportSettings
| ObjectType::Search
| ObjectType::Security
| ObjectType::SenderAuth
| ObjectType::Sharing
| ObjectType::SieveSystemInterpreter
| ObjectType::SieveSystemScript
| ObjectType::SieveUserInterpreter
| ObjectType::SieveUserScript
| ObjectType::SpamClassifier
| ObjectType::SpamDnsblServer
| ObjectType::SpamDnsblSettings
| ObjectType::SpamFileExtension
| ObjectType::SpamPyzor
| ObjectType::SpamRule
| ObjectType::SpamSettings
| ObjectType::SpamTag
| ObjectType::SpfReportSettings
| ObjectType::SystemSettings
| ObjectType::TaskManager
| ObjectType::TlsReportSettings
| ObjectType::Tracer
| ObjectType::WebDav
| ObjectType::WebHook => Some(object),
_ => None,
}
}
impl Server {
/// Applies a stored registry write to `object` to the running settings,
/// and on success tells the other nodes to do the same. Returns None when
/// the write needs no reload, Some(Ok(())) when it was applied, and
/// Some(Err(reason)) when the reload was refused (the write stays stored;
/// ReloadSettings reports the same errors).
pub async fn reload_after_write(&self, object: ObjectType) -> Option<Result<(), String>> {
let target = write_reload_target(object)?;
let change = RegistryChange::Reload(target);
if matches!(
target,
ObjectType::Certificate | ObjectType::StoreLookup | ObjectType::BlockedIp
) {
// Cheap, and limited to their own objects
let result = self.reload_and_broadcast(change).await;
return Some(result);
}
let gate = &self.inner.data.settings_reload;
let ticket = gate
.requested
.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
+ 1;
let mut state = gate.state.lock().await;
if state.completed >= ticket {
// A reload that started after this write was stored has run
return Some(state.refused.clone().map_or(Ok(()), Err));
}
let covers = gate.requested.load(std::sync::atomic::Ordering::SeqCst);
let result = self.reload_and_broadcast(change).await;
state.completed = covers;
state.refused = result.clone().err();
Some(result)
}
async fn reload_and_broadcast(&self, change: RegistryChange) -> Result<(), String> {
match Box::pin(self.reload_registry(change)).await {
Ok(reload) if !reload.has_errors() => {
reload.log();
self.cluster_broadcast(BroadcastEvent::RegistryChange(change))
.await;
Ok(())
}
Ok(reload) => {
reload.log();
let reason = describe_reload_errors(&reload.errors);
trc::event!(
Registry(trc::RegistryEvent::BuildWarning),
Details = "Settings didn't reload after a registry write",
Reason = reason.clone(),
);
Err(reason)
}
Err(err) => {
let reason = err.to_string();
trc::error!(err.details("Failed to reload settings after a registry write"));
Err(reason)
}
}
}
}
/// inbuxa: a refused reload's errors in a sentence: the first one, naming its
/// object, and how many more there are.
pub fn describe_reload_errors(errors: &[Error]) -> String {
let mut description = match errors.first() {
Some(Error::Build { object_id, message }) => format!("{object_id}: {message}"),
Some(Error::Validation { object_id, errors }) => format!(
"{object_id}: {}",
errors
.iter()
.map(|err| err.to_string())
.collect::<Vec<_>>()
.join("; ")
),
Some(Error::Internal {
object_id: Some(object_id),
error,
}) => format!("{object_id}: {error}"),
Some(Error::Internal { error, .. }) => error.to_string(),
Some(Error::NotFound { object_id }) => format!("{object_id} was not found"),
None => String::new(),
};
let more = errors.len().saturating_sub(1);
if more > 0 {
description.push_str(&format!(" ({more} more in the server log.)"));
}
description
}
+2
View File
@@ -93,6 +93,7 @@ impl Data {
registry_id_gen: id_generator.clone(), registry_id_gen: id_generator.clone(),
span_id_gen: id_generator, span_id_gen: id_generator,
queue_status: true.into(), queue_status: true.into(),
settings_reload: Default::default(),
applications, applications,
logos: Default::default(), logos: Default::default(),
smtp_connectors: TlsConnectors::try_new().failed("Failed to build TLS connectors"), smtp_connectors: TlsConnectors::try_new().failed("Failed to build TLS connectors"),
@@ -235,6 +236,7 @@ impl Default for Data {
span_id_gen: Default::default(), span_id_gen: Default::default(),
registry_id_gen: Default::default(), registry_id_gen: Default::default(),
queue_status: true.into(), queue_status: true.into(),
settings_reload: Default::default(),
applications: WebApplications::new(), applications: WebApplications::new(),
logos: Default::default(), logos: Default::default(),
smtp_connectors: TlsConnectors::try_new().unwrap(), smtp_connectors: TlsConnectors::try_new().unwrap(),
+2
View File
@@ -161,6 +161,8 @@ pub struct Data {
pub span_id_gen: SnowflakeIdGenerator, pub span_id_gen: SnowflakeIdGenerator,
pub registry_id_gen: SnowflakeIdGenerator, pub registry_id_gen: SnowflakeIdGenerator,
pub queue_status: AtomicBool, pub queue_status: AtomicBool,
// inbuxa: coalesces the settings reloads registry writes trigger
pub settings_reload: cache::reload::SettingsReloadGate,
pub applications: WebApplications, pub applications: WebApplications,
pub logos: Mutex<AHashMap<Box<str>, LogoCache>>, pub logos: Mutex<AHashMap<Box<str>, LogoCache>>,
+20
View File
@@ -2,6 +2,8 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]> * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/ */
use super::ahash_is_empty; use super::ahash_is_empty;
@@ -71,6 +73,23 @@ pub struct SetResponse<T: JmapObject> {
#[serde(rename = "notDestroyed")] #[serde(rename = "notDestroyed")]
#[serde(skip_serializing_if = "VecMap::is_empty")] #[serde(skip_serializing_if = "VecMap::is_empty")]
pub not_destroyed: VecMap<MaybeInvalid<Id>, SetError<T::Property>>, pub not_destroyed: VecMap<MaybeInvalid<Id>, SetError<T::Property>>,
// inbuxa: on a registry write that changes the running settings, whether
// the server applied it
#[serde(rename = "x:settingsReload")]
#[serde(skip_serializing_if = "Option::is_none")]
pub settings_reload: Option<SettingsReload>,
}
/// inbuxa: the settings reload that followed a registry write.
#[derive(Debug, Clone, serde::Serialize)]
pub struct SettingsReload {
/// The running settings (here and, through the cluster, on every node)
/// include the write.
pub applied: bool,
/// Why they don't, when they don't.
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
} }
impl<'de, T: JmapObject> DeserializeArguments<'de> for SetRequest<'de, T> { impl<'de, T: JmapObject> DeserializeArguments<'de> for SetRequest<'de, T> {
@@ -199,6 +218,7 @@ impl<T: JmapObject> SetResponse<T> {
not_created: VecMap::new(), not_created: VecMap::new(),
not_updated: VecMap::new(), not_updated: VecMap::new(),
not_destroyed: VecMap::new(), not_destroyed: VecMap::new(),
settings_reload: None,
}) })
} else { } else {
Err(trc::JmapEvent::RequestTooLarge.into_err()) Err(trc::JmapEvent::RequestTooLarge.into_err())
+4 -24
View File
@@ -580,29 +580,9 @@ async fn dmarc_troubleshoot(
/// settings weren't applied; upstream passed on the first error's bare message /// settings weren't applied; upstream passed on the first error's bare message
/// ("Invalid address: ..."), which read like a problem with the request. /// ("Invalid address: ..."), which read like a problem with the request.
fn reload_refused(errors: Vec<registry::types::error::Error>) -> SetError<Property> { fn reload_refused(errors: Vec<registry::types::error::Error>) -> SetError<Property> {
use registry::types::error::Error; let description = format!(
let more = errors.len().saturating_sub(1); "Settings were not reloaded. {}",
let mut description = match errors.first() { common::cache::reload::describe_reload_errors(&errors)
Some(Error::Build { object_id, message }) => format!("{object_id}: {message}"), );
Some(Error::Validation { object_id, errors }) => format!(
"{object_id}: {}",
errors
.iter()
.map(|err| err.to_string())
.collect::<Vec<_>>()
.join("; ")
),
Some(Error::Internal {
object_id: Some(object_id),
error,
}) => format!("{object_id}: {error}"),
Some(Error::Internal { error, .. }) => error.to_string(),
Some(Error::NotFound { object_id }) => format!("{object_id} was not found"),
None => String::new(),
};
description.insert_str(0, "Settings were not reloaded. ");
if more > 0 {
description.push_str(&format!(" ({more} more in the server log.)"));
}
map_bootstrap_error(errors).with_description(description) map_bootstrap_error(errors).with_description(description)
} }
+19 -25
View File
@@ -38,7 +38,7 @@ use directory::core::secret::{hash_secret, is_password_hash};
use http_proto::HttpSessionData; use http_proto::HttpSessionData;
use jmap_proto::{ use jmap_proto::{
error::set::{SetError, SetErrorType}, error::set::{SetError, SetErrorType},
method::set::{SetRequest, SetResponse}, method::set::{SetRequest, SetResponse, SettingsReload},
object::registry::Registry, object::registry::Registry,
references::resolve::ResolveCreatedReference, references::resolve::ResolveCreatedReference,
request::{IntoValid, MaybeInvalid}, request::{IntoValid, MaybeInvalid},
@@ -931,34 +931,28 @@ impl RegistrySet for Server {
} }
}; };
// inbuxa: DIR-17: a directory or the server default applies on the // inbuxa: a write to an object the running settings are built from
// next request, here and on every node // applies at once, here and on every node (DIR-17 did this for
if matches!( // directories and the server default; now it covers every such object)
object_type, let mut result = result;
ObjectType::Directory | ObjectType::Authentication if let Ok(response) = &mut result
) && let Ok(response) = &result
&& (!response.created.is_empty() && (!response.created.is_empty()
|| !response.updated.is_empty() || !response.updated.is_empty()
|| !response.destroyed.is_empty()) || !response.destroyed.is_empty())
&& let Some(reload) = self.reload_after_write(object_type).await
{ {
let change = common::ipc::RegistryChange::Reload(ObjectType::Directory); response.settings_reload = Some(match reload {
match Box::pin(self.reload_registry(change)).await { Ok(()) => SettingsReload {
Ok(reload) if !reload.has_errors() => { applied: true,
self.cluster_broadcast(common::ipc::BroadcastEvent::RegistryChange(change)) description: None,
.await; },
} Err(reason) => SettingsReload {
Ok(reload) => { applied: false,
// inbuxa: name what stopped it description: Some(format!(
reload.log(); "Saved, but the running settings were not reloaded. {reason}"
trc::event!( )),
Registry(trc::RegistryEvent::BuildWarning), },
Details = "Settings didn't reload after a directory change", });
)
}
Err(err) => {
trc::error!(err.details("Failed to reload directories"));
}
}
} }
result result
} }
+44 -1
View File
@@ -2,6 +2,8 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]> * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/ */
use crate::{ use crate::{
@@ -11,6 +13,7 @@ use crate::{
server::TestServerBuilder, server::TestServerBuilder,
}, },
}; };
use common::BuildServer;
use imap_proto::ResponseType; use imap_proto::ResponseType;
use registry::{ use registry::{
schema::{ schema::{
@@ -18,7 +21,8 @@ use registry::{
prelude::{ObjectType, Property, SocketAddr}, prelude::{ObjectType, Property, SocketAddr},
structs::{ structs::{
ClusterListenerGroup, ClusterListenerGroupProperties, ClusterRole, ClusterTaskGroup, ClusterListenerGroup, ClusterListenerGroupProperties, ClusterRole, ClusterTaskGroup,
Coordinator, Imap, NatsCoordinator, NetworkListener, RedisStore, Coordinator, Imap, MtaDeliverySchedule, MtaVirtualQueue, NatsCoordinator,
NetworkListener, RedisStore,
}, },
}, },
types::map::Map, types::map::Map,
@@ -209,6 +213,45 @@ pub async fn cluster_tests() {
Some("John Doe") Some("John Doe")
); );
// inbuxa: a settings write applies on every node, no ReloadSettings
let queue_id = admin
.registry_create_object(MtaVirtualQueue {
name: "clusterq".into(),
threads_per_node: 1,
description: None,
})
.await;
admin
.registry_create_object(MtaDeliverySchedule {
name: "cluster-autoreload".into(),
queue_id,
..Default::default()
})
.await;
for (node_id, test) in servers.iter().enumerate() {
let started = std::time::Instant::now();
while !test
.server
.inner
.build_server()
.core
.smtp
.queue
.queue_strategy
.contains_key("cluster-autoreload")
{
assert!(
started.elapsed() < std::time::Duration::from_secs(5),
"node {node_id} didn't pick up the new delivery schedule"
);
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
println!(
"Node {node_id} has the new delivery schedule after {} ms",
started.elapsed().as_millis()
);
}
// Run IMAP idle tests across nodes // Run IMAP idle tests across nodes
let mut node1_client = imap_client("[email protected]", "this is john's secret", 1).await; let mut node1_client = imap_client("[email protected]", "this is john's secret", 1).await;
let mut node2_client = imap_client("[email protected]", "this is john's secret", 2).await; let mut node2_client = imap_client("[email protected]", "this is john's secret", 2).await;
+40 -17
View File
@@ -2,6 +2,8 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]> * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/ */
use crate::utils::server::TestServer; use crate::utils::server::TestServer;
@@ -40,15 +42,23 @@ pub mod vrfy;
const EVENT_TIMEOUT: Duration = Duration::from_secs(5); const EVENT_TIMEOUT: Duration = Duration::from_secs(5);
impl TestServer { impl TestServer {
// inbuxa: registry writes reload the settings, and each reload sends the
// queue a ReloadSettings; read_event, try_read_event and assert_no_events
// pass over those (expect_reload_settings still waits for one)
pub async fn read_event(&mut self) -> QueueEvent { pub async fn read_event(&mut self) -> QueueEvent {
if let Some(event) = self.queue_events.pop_front() { while let Some(event) = self.queue_events.pop_front() {
return event; if !event.is_reload_settings() {
return event;
}
} }
match tokio::time::timeout(EVENT_TIMEOUT, self.queue_rx.recv()).await { loop {
Ok(Some(event)) => event, match tokio::time::timeout(EVENT_TIMEOUT, self.queue_rx.recv()).await {
Ok(None) => panic!("Channel closed."), Ok(Some(event)) if event.is_reload_settings() => (),
Err(_) => panic!("No queue event received."), Ok(Some(event)) => return event,
Ok(None) => panic!("Channel closed."),
Err(_) => panic!("No queue event received."),
}
} }
} }
@@ -78,26 +88,39 @@ impl TestServer {
} }
pub async fn try_read_event(&mut self) -> Option<QueueEvent> { pub async fn try_read_event(&mut self) -> Option<QueueEvent> {
if let Some(event) = self.queue_events.pop_front() { while let Some(event) = self.queue_events.pop_front() {
return Some(event); if !event.is_reload_settings() {
return Some(event);
}
} }
match tokio::time::timeout(EVENT_TIMEOUT, self.queue_rx.recv()).await { loop {
Ok(Some(event)) => Some(event), match tokio::time::timeout(EVENT_TIMEOUT, self.queue_rx.recv()).await {
Ok(None) => panic!("Channel closed."), Ok(Some(event)) if event.is_reload_settings() => (),
Err(_) => None, Ok(Some(event)) => return Some(event),
Ok(None) => panic!("Channel closed."),
Err(_) => return None,
}
} }
} }
pub fn assert_no_events(&mut self) { pub fn assert_no_events(&mut self) {
if let Some(event) = self.queue_events.pop_front() { if let Some(event) = self
.queue_events
.iter()
.find(|event| !event.is_reload_settings())
{
panic!("Expected empty queue but got {event:?}"); panic!("Expected empty queue but got {event:?}");
} }
self.queue_events.clear();
match self.queue_rx.try_recv() { loop {
Err(TryRecvError::Empty) => (), match self.queue_rx.try_recv() {
Ok(event) => panic!("Expected empty queue but got {event:?}"), Ok(event) if event.is_reload_settings() => (),
Err(err) => panic!("Queue error: {err:?}"), Err(TryRecvError::Empty) => break,
Ok(event) => panic!("Expected empty queue but got {event:?}"),
Err(err) => panic!("Queue error: {err:?}"),
}
} }
} }
+189
View File
@@ -0,0 +1,189 @@
/*
* SPDX-FileCopyrightText: 2026 Coffey Labs
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
// inbuxa: a registry write to an object the running settings are built from
// applies without an x:Action ReloadSettings, and the set response says so.
use crate::utils::{
jmap::JmapResponse,
server::{TestServer, TestServerBuilder},
};
use common::BuildServer;
use registry::schema::{
enums::TracingLevel,
prelude::ObjectType,
structs::{
CertificateManagement, DkimManagement, DnsManagement, Domain, Expression,
MtaDeliverySchedule, MtaStageAuth, MtaVirtualQueue, Tracer, TracerStdout,
},
};
use serde_json::Value;
#[tokio::test(flavor = "multi_thread")]
pub async fn settings_reload_tests() {
let mut test = TestServerBuilder::new("settings_reload_tests")
.await
.with_default_listeners()
.await
.with_object(MtaStageAuth {
require: Expression {
else_: "false".to_string(),
..Default::default()
},
..Default::default()
})
.await
.build()
.await;
let admin = test
.create_user_account(
"admin",
"[email protected]",
"these_pretzels_are_making_me_thirsty",
&[],
"Admin",
)
.await;
test.account("admin")
.assign_roles_to_account(admin.id(), &["user", "system"])
.await;
test.insert_account(admin);
test_write_applies(&test).await;
if test.is_reset() {
test.temp_dir.delete();
}
}
async fn test_write_applies(test: &TestServer) {
println!("Running settings reload after registry writes...");
let admin = test.account("[email protected]");
// A delivery schedule is in use as soon as it is saved
let response = admin
.registry_create([MtaVirtualQueue {
name: "autorld".into(),
threads_per_node: 2,
description: None,
}])
.await;
assert_applied(&response);
let queue_id = response.created_id(0);
assert!(!has_schedule(test, "autoreload-schedule"));
let response = admin
.registry_create([MtaDeliverySchedule {
name: "autoreload-schedule".into(),
queue_id,
..Default::default()
}])
.await;
assert_applied(&response);
assert!(has_schedule(test, "autoreload-schedule"));
// Destroyed, it's gone at once too
let schedule_id = response.created_id(0);
let response = admin
.registry_destroy(ObjectType::MtaDeliverySchedule, [schedule_id])
.await;
assert_applied(&response);
assert!(!has_schedule(test, "autoreload-schedule"));
// Concurrent writes all end up in the running settings
let names = (0..8)
.map(|i| format!("autoreload-{i}"))
.collect::<Vec<_>>();
let mut writes = Vec::new();
for name in &names {
writes.push(admin.registry_create([MtaDeliverySchedule {
name: name.clone(),
queue_id,
..Default::default()
}]));
}
let mut schedule_ids = Vec::new();
for response in futures::future::join_all(writes).await {
assert_applied(&response);
schedule_ids.push(response.created_id(0));
}
for name in &names {
assert!(has_schedule(test, name), "{name} missing");
}
// Several objects in one request: one reload
let response = admin
.registry_destroy(ObjectType::MtaDeliverySchedule, schedule_ids.iter())
.await;
assert_applied(&response);
for name in &names {
assert!(!has_schedule(test, name), "{name} still present");
}
// A write whose reload fails is stored, and the response says the
// settings weren't reloaded: only one console tracer is allowed.
let response = admin
.registry_create([
Tracer::Stdout(TracerStdout {
enable: true,
level: TracingLevel::Error,
..Default::default()
}),
Tracer::Stdout(TracerStdout {
enable: true,
level: TracingLevel::Error,
..Default::default()
}),
])
.await;
let reload = settings_reload(&response).expect("x:settingsReload missing");
assert_eq!(reload["applied"], Value::Bool(false), "{response:?}");
let description = reload["description"].as_str().unwrap_or_default();
assert!(
description.starts_with("Saved, but the running settings were not reloaded. ")
&& description.contains("Only one console tracer is allowed"),
"{description}"
);
let tracer_ids = [response.created_id(0), response.created_id(1)];
let response = admin
.registry_destroy(ObjectType::Tracer, tracer_ids.iter())
.await;
assert_applied(&response);
// Data that isn't part of the running settings doesn't reload them
let response = admin
.registry_create([Domain {
name: "autoreload.example.org".into(),
certificate_management: CertificateManagement::Manual,
dns_management: DnsManagement::Manual,
dkim_management: DkimManagement::Manual,
..Default::default()
}])
.await;
assert!(settings_reload(&response).is_none(), "{response:?}");
}
fn settings_reload(response: &JmapResponse) -> Option<&Value> {
response.pointer("/methodResponses/0/1/x:settingsReload")
}
fn assert_applied(response: &JmapResponse) {
assert_eq!(
settings_reload(response),
Some(&serde_json::json!({"applied": true})),
"{response:?}"
);
}
fn has_schedule(test: &TestServer, name: &str) -> bool {
test.server
.inner
.build_server()
.core
.smtp
.queue
.queue_strategy
.contains_key(name)
}
+1
View File
@@ -11,6 +11,7 @@ pub mod authentication;
pub mod ai; pub mod ai;
pub mod ai_calibration; pub mod ai_calibration;
pub mod authorization; pub mod authorization;
pub mod auto_reload; // inbuxa: registry writes apply at once
pub mod branding; pub mod branding;
pub mod crypto; pub mod crypto;
pub mod delivery; pub mod delivery;