The switch now reaches the running server

The join: the policy decides, features owns the listener objects,
ListenerControl owns the running sockets, and only Server has both.

Server::set_protocol_policy is what a click performs. It applies the locks
to what was asked before storing anything (LP-21), so what is recorded is
what the server allows. Closing removes each listener object and then stops
its socket; opening puts the object back and then spawns it. The order is
the point in both directions -- a socket stopped while its object remains
returns on the next restart, and a socket spawned before its object exists
has nothing to come back to.

saved_listeners is carried over from the stored policy rather than taken
from the request. A client never sets it, and a /set that omitted it would
otherwise lose the listeners still waiting to come back.

Putting a listener back has to bind a fresh socket, so it re-parses from
the registry -- the objects are already back by then -- rather than trying
to revive the saved one. Only main knows which session manager a protocol
wants, so it leaves a spawner behind at startup and spawn_listener is now
shared between that and the initial spawn. Without a spawner a restored
listener is reported as pending a restart rather than promised, which is
what the test servers will see.

A listener that cannot be put back does not stop the others and stays
saved for another try (LP-5).

Still nothing an operator can reach: no JMAP method calls this yet, and no
sign-in is refused. What it does do is close and reopen a port on a
running server, which is the part that did not exist this morning.
This commit is contained in:
2026-09-20 15:27:32 -07:00
parent 08f12fa158
commit 3b29ca3571
4 changed files with 290 additions and 34 deletions
+65 -33
View File
@@ -9,14 +9,21 @@
#![warn(clippy::cast_possible_wrap)]
#![warn(clippy::cast_sign_loss)]
use common::{BuildServer, config::server::ServerProtocol, manager::boot::BootManager};
use common::{
BuildServer, Inner,
config::server::{Listener, ServerProtocol},
manager::boot::BootManager,
network::TcpAcceptor,
};
use http::HttpSessionManager;
use imap::core::ImapSessionManager;
use managesieve::core::ManageSieveSessionManager;
use pop3::Pop3SessionManager;
use services::{StartServices, broadcast::subscriber::spawn_broadcast_subscriber};
use smtp::{StartQueueManager, core::SmtpSessionManager};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::watch;
use trc::Collector;
use utils::wait_for_shutdown;
@@ -75,43 +82,24 @@ async fn main() -> std::io::Result<()> {
// rest accepting (legacy-protocols LP-2). The registry lives in `Data` and
// so outlives the listeners, which it must: it owns the sending ends.
let listener_control = &init.inner.data.listener_control;
let spawn_inner = init.inner.clone();
let (shutdown_tx, shutdown_rx) =
init.servers
.spawn_with_control(listener_control, |server, acceptor, shutdown_rx| {
match &server.protocol {
ServerProtocol::Smtp | ServerProtocol::Lmtp => server.spawn(
SmtpSessionManager::new(init.inner.clone()),
init.inner.clone(),
acceptor,
shutdown_rx,
),
ServerProtocol::Http => server.spawn(
HttpSessionManager::new(init.inner.clone()),
init.inner.clone(),
acceptor,
shutdown_rx,
),
ServerProtocol::Imap => server.spawn(
ImapSessionManager::new(init.inner.clone()),
init.inner.clone(),
acceptor,
shutdown_rx,
),
ServerProtocol::Pop3 => server.spawn(
Pop3SessionManager::new(init.inner.clone()),
init.inner.clone(),
acceptor,
shutdown_rx,
),
ServerProtocol::ManageSieve => server.spawn(
ManageSieveSessionManager::new(init.inner.clone()),
init.inner.clone(),
acceptor,
shutdown_rx,
),
};
spawn_listener(&spawn_inner, server, acceptor, shutdown_rx);
});
// Leave behind how to spawn a listener, so putting one back opens its port
// without a restart (LP-5). Only this file knows the session manager for a
// protocol, so only this file can say.
let spawn_inner = init.inner.clone();
init.inner
.data
.listener_control
.set_spawner(Box::new(move |server, acceptor, shutdown_rx| {
spawn_listener(&spawn_inner, server, acceptor, shutdown_rx);
}));
// Start broadcast subscriber
let inner = init.inner.clone();
spawn_broadcast_subscriber(init.inner, shutdown_rx);
@@ -132,3 +120,47 @@ async fn main() -> std::io::Result<()> {
Ok(())
}
/// Starts one listener under the session manager its protocol calls for.
///
/// Used twice: once for every listener at startup, and again whenever the
/// legacy-protocols switch puts a listener back (LP-5).
fn spawn_listener(
inner: &Arc<Inner>,
server: Listener,
acceptor: TcpAcceptor,
shutdown_rx: watch::Receiver<bool>,
) {
match &server.protocol {
ServerProtocol::Smtp | ServerProtocol::Lmtp => server.spawn(
SmtpSessionManager::new(inner.clone()),
inner.clone(),
acceptor,
shutdown_rx,
),
ServerProtocol::Http => server.spawn(
HttpSessionManager::new(inner.clone()),
inner.clone(),
acceptor,
shutdown_rx,
),
ServerProtocol::Imap => server.spawn(
ImapSessionManager::new(inner.clone()),
inner.clone(),
acceptor,
shutdown_rx,
),
ServerProtocol::Pop3 => server.spawn(
Pop3SessionManager::new(inner.clone()),
inner.clone(),
acceptor,
shutdown_rx,
),
ServerProtocol::ManageSieve => server.spawn(
ManageSieveSessionManager::new(inner.clone()),
inner.clone(),
acceptor,
shutdown_rx,
),
}
}