Files
mailks/lib.rs
T
zhenyi 3251fa08e3 refactor: extract EmailksServer as library, thin main.rs to bootstrap-only
- Add EmailksServer / EmailksServerBuilder to lib.rs
- Expose serve() / serve_with_shutdown() for embedding
- main.rs now only handles etcd config and delegates to library
2026-06-12 21:36:42 +08:00

172 lines
4.6 KiB
Rust

pub mod config;
pub mod email;
pub mod email_build;
pub mod error;
pub mod etcd;
pub mod queue;
pub mod server;
pub mod status;
pub(crate) const ENV_PREFIX: &str = "APP_SMTP_";
pub mod pb {
pub mod email {
pub mod v1 {
include!(concat!(env!("OUT_DIR"), "/pb/email.v1.rs"));
}
}
}
use std::future::Future;
use std::net::SocketAddr;
use crate::config::AppConfig;
use crate::email::EmailSender;
use crate::pb::email::v1::email_service_server::EmailServiceServer;
use crate::queue::EmailQueue;
use crate::server::EmailServiceImpl;
use tonic::transport::Server as TonicServer;
use tracing::{error, info};
const DEFAULT_QUEUE_CAPACITY: usize = 1_000;
/// A ready-to-run emailks gRPC server.
///
/// Use [`EmailksServerBuilder`] to construct an instance from configuration.
pub struct EmailksServer {
service: EmailServiceImpl,
worker_handle: tokio::task::JoinHandle<()>,
addr: SocketAddr,
}
/// Builder for [`EmailksServer`].
///
/// # Examples
///
/// ```no_run
/// use emailks::{EmailksServer, config::AppConfig};
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let config = AppConfig::from_env()?;
/// let server = EmailksServer::builder()
/// .config(config)
/// .build()
/// .await?;
/// server.serve().await?;
/// # Ok(())
/// # }
/// ```
pub struct EmailksServerBuilder {
config: Option<AppConfig>,
}
impl EmailksServer {
/// Create a new builder.
pub fn builder() -> EmailksServerBuilder {
EmailksServerBuilder::default()
}
/// Start the gRPC server and block until Ctrl+C (or SIGTERM on Unix).
pub async fn serve(self) -> Result<(), Box<dyn std::error::Error>> {
self.serve_with_shutdown(shutdown_signal()).await
}
/// Start the gRPC server and block until the provided `shutdown` future resolves.
///
/// In-flight requests are drained before the server returns.
pub async fn serve_with_shutdown(
self,
shutdown: impl Future<Output = ()>,
) -> Result<(), Box<dyn std::error::Error>> {
let svc = self.service;
let addr = self.addr;
let (health, health_svc) = tonic_health::server::health_reporter();
health
.set_serving::<EmailServiceServer<EmailServiceImpl>>()
.await;
info!(%addr, "gRPC server starting");
TonicServer::builder()
.add_service(health_svc)
.add_service(EmailServiceServer::new(svc))
.serve_with_shutdown(addr, shutdown)
.await?;
info!("server stopped");
if let Err(e) = self.worker_handle.await {
error!(error = %e, "worker task panicked");
}
Ok(())
}
}
impl EmailksServerBuilder {
/// Set the server configuration.
pub fn config(mut self, config: AppConfig) -> Self {
self.config = Some(config);
self
}
/// Build the server, creating the SMTP sender, queue, worker, and gRPC service.
pub async fn build(self) -> Result<EmailksServer, Box<dyn std::error::Error>> {
let config = self
.config
.unwrap_or_else(|| AppConfig::from_env().expect("failed to load emailks config"));
info!(
host = %config.smtp.host,
port = config.smtp.port,
"smtp config loaded"
);
let sender = EmailSender::new(config.smtp)?;
let (queue, worker) = match config.queue_capacity {
Some(0) => {
info!("creating unbounded queue");
EmailQueue::unbounded()
}
Some(cap) => {
info!(capacity = cap, "creating bounded queue");
EmailQueue::bounded(cap)
}
None => {
info!(
capacity = DEFAULT_QUEUE_CAPACITY,
"creating bounded queue (default)"
);
EmailQueue::bounded(DEFAULT_QUEUE_CAPACITY)
}
};
let store = queue.status_store().clone();
let worker_handle = worker.spawn(move |job| {
let s = sender.clone();
async move { s.send_job(&job).await }
});
let addr = config.listen_addr;
let service = EmailServiceImpl::new(queue, store);
Ok(EmailksServer {
service,
worker_handle,
addr,
})
}
}
impl Default for EmailksServerBuilder {
fn default() -> Self {
Self { config: None }
}
}
async fn shutdown_signal() {
match tokio::signal::ctrl_c().await {
Ok(()) => info!("shutdown signal received"),
Err(err) => error!(%err, "failed to install CTRL+C handler"),
}
}