Global state with OnceLock and lazy initialization
The Singleton pattern ensures a class has only one instance and provides a global point of access to it. In Rust, this pattern requires careful consideration due to the ownership system and thread safety requirements.
The Rust perspective:Sync and SendOnceCell or lazy_staticuse std::sync::OnceLock;
// Modern Rust singleton using OnceLock (stable since Rust 1.70)
static CONFIG: OnceLock<Configuration> = OnceLock::new();
pub struct Configuration {
pub database_url: String,
pub max_connections: u32,
pub debug_mode: bool,
}
impl Configuration {
pub fn global() -> &'static Configuration {
CONFIG.get_or_init(|| {
Configuration {
database_url: std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgres://localhost/app".into()),
max_connections: 10,
debug_mode: cfg!(debug_assertions),
}
})
}
}
A comprehensive configuration system that loads from environment and files.
use std::sync::OnceLock;
use std::collections::HashMap;
use std::path::PathBuf;
/// Global configuration singleton
static APP_CONFIG: OnceLock<AppConfig> = OnceLock::new();
#[derive(Debug, Clone)]
pub struct AppConfig {
// Server settings
pub host: String,
pub port: u16,
pub workers: usize,
// Database settings
pub database: DatabaseConfig,
// Feature flags
pub features: HashMap<String, bool>,
// Environment
pub environment: Environment,
}
#[derive(Debug, Clone)]
pub struct DatabaseConfig {
pub url: String,
pub max_connections: u32,
pub min_connections: u32,
pub connection_timeout_secs: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Environment {
Development,
Staging,
Production,
}
impl AppConfig {
/// Access the global configuration
pub fn global() -> &'static AppConfig {
APP_CONFIG.get_or_init(Self::load_config)
}
/// Initialize with custom config (useful for testing)
pub fn init(config: AppConfig) -> Result<(), AppConfig> {
APP_CONFIG.set(config)
}
fn load_config() -> AppConfig {
let environment = match std::env::var("APP_ENV")
.unwrap_or_else(|_| "development".into())
.to_lowercase()
.as_str()
{
"production" | "prod" => Environment::Production,
"staging" | "stage" => Environment::Staging,
_ => Environment::Development,
};
let default_workers = num_cpus::get();
AppConfig {
host: std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".into()),
port: std::env::var("PORT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(8080),
workers: std::env::var("WORKERS")
.ok()
.and_then(|w| w.parse().ok())
.unwrap_or(default_workers),
database: DatabaseConfig {
url: std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgres://localhost/app".into()),
max_connections: std::env::var("DB_MAX_CONN")
.ok()
.and_then(|c| c.parse().ok())
.unwrap_or(10),
min_connections: std::env::var("DB_MIN_CONN")
.ok()
.and_then(|c| c.parse().ok())
.unwrap_or(1),
connection_timeout_secs: 30,
},
features: Self::load_feature_flags(),
environment,
}
}
fn load_feature_flags() -> HashMap<String, bool> {
let mut flags = HashMap::new();
// Load from environment variables prefixed with FEATURE_
for (key, value) in std::env::vars() {
if let Some(feature_name) = key.strip_prefix("FEATURE_") {
let enabled = matches!(
value.to_lowercase().as_str(),
"true" | "1" | "yes" | "on"
);
flags.insert(feature_name.to_lowercase(), enabled);
}
}
flags
}
/// Check if a feature flag is enabled
pub fn is_feature_enabled(&self, feature: &str) -> bool {
self.features.get(feature).copied().unwrap_or(false)
}
/// Check if running in production
pub fn is_production(&self) -> bool {
self.environment == Environment::Production
}
}
// Usage
fn main() {
let config = AppConfig::global();
println!("Server starting on {}:{}", config.host, config.port);
println!("Environment: {:?}", config.environment);
println!("Database: {}", config.database.url);
if config.is_feature_enabled("new_dashboard") {
println!("New dashboard feature is enabled!");
}
}
A thread-safe logging system with different output targets.
use std::sync::{OnceLock, Mutex, Arc};
use std::io::{self, Write};
use std::fs::{File, OpenOptions};
use std::path::Path;
use chrono::{DateTime, Utc};
static LOGGER: OnceLock<Logger> = OnceLock::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LogLevel {
Trace = 0,
Debug = 1,
Info = 2,
Warn = 3,
Error = 4,
}
impl std::fmt::Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LogLevel::Trace => write!(f, "TRACE"),
LogLevel::Debug => write!(f, "DEBUG"),
LogLevel::Info => write!(f, "INFO"),
LogLevel::Warn => write!(f, "WARN"),
LogLevel::Error => write!(f, "ERROR"),
}
}
}
pub trait LogTarget: Send + Sync {
fn write(&self, level: LogLevel, message: &str, timestamp: DateTime<Utc>);
fn flush(&self);
}
/// Console output target
pub struct ConsoleTarget {
use_colors: bool,
}
impl ConsoleTarget {
pub fn new(use_colors: bool) -> Self {
Self { use_colors }
}
fn level_color(&self, level: LogLevel) -> &'static str {
if !self.use_colors {
return "";
}
match level {
LogLevel::Trace => "\x1b[90m", // Gray
LogLevel::Debug => "\x1b[36m", // Cyan
LogLevel::Info => "\x1b[32m", // Green
LogLevel::Warn => "\x1b[33m", // Yellow
LogLevel::Error => "\x1b[31m", // Red
}
}
fn reset_color(&self) -> &'static str {
if self.use_colors { "\x1b[0m" } else { "" }
}
}
impl LogTarget for ConsoleTarget {
fn write(&self, level: LogLevel, message: &str, timestamp: DateTime<Utc>) {
let output = format!(
"{}{} [{}] {}{}\n",
self.level_color(level),
timestamp.format("%Y-%m-%d %H:%M:%S%.3f"),
level,
message,
self.reset_color()
);
let mut handle = if level >= LogLevel::Warn {
io::stderr()
} else {
io::stdout()
};
let _ = handle.write_all(output.as_bytes());
}
fn flush(&self) {
let _ = io::stdout().flush();
let _ = io::stderr().flush();
}
}
/// File output target
pub struct FileTarget {
file: Mutex<File>,
}
impl FileTarget {
pub fn new<P: AsRef<Path>>(path: P) -> io::Result<Self> {
let file = OpenOptions::new()
.create(true)
.append(true)
.open(path)?;
Ok(Self {
file: Mutex::new(file),
})
}
}
impl LogTarget for FileTarget {
fn write(&self, level: LogLevel, message: &str, timestamp: DateTime<Utc>) {
let output = format!(
"{} [{}] {}\n",
timestamp.format("%Y-%m-%d %H:%M:%S%.3f"),
level,
message
);
if let Ok(mut file) = self.file.lock() {
let _ = file.write_all(output.as_bytes());
}
}
fn flush(&self) {
if let Ok(mut file) = self.file.lock() {
let _ = file.flush();
}
}
}
pub struct Logger {
min_level: LogLevel,
targets: Vec<Arc<dyn LogTarget>>,
}
impl Logger {
/// Get the global logger instance
pub fn global() -> &'static Logger {
LOGGER.get_or_init(|| {
Logger {
min_level: LogLevel::Info,
targets: vec![Arc::new(ConsoleTarget::new(true))],
}
})
}
/// Initialize logger with custom configuration
pub fn init(min_level: LogLevel, targets: Vec<Arc<dyn LogTarget>>) -> Result<(), Logger> {
LOGGER.set(Logger { min_level, targets })
}
pub fn log(&self, level: LogLevel, message: &str) {
if level < self.min_level {
return;
}
let timestamp = Utc::now();
for target in &self.targets {
target.write(level, message, timestamp);
}
}
pub fn trace(&self, message: &str) { self.log(LogLevel::Trace, message); }
pub fn debug(&self, message: &str) { self.log(LogLevel::Debug, message); }
pub fn info(&self, message: &str) { self.log(LogLevel::Info, message); }
pub fn warn(&self, message: &str) { self.log(LogLevel::Warn, message); }
pub fn error(&self, message: &str) { self.log(LogLevel::Error, message); }
pub fn flush(&self) {
for target in &self.targets {
target.flush();
}
}
}
// Convenience macros
#[macro_export]
macro_rules! log_info {
($($arg:tt)*) => {
$crate::Logger::global().info(&format!($($arg)*))
};
}
#[macro_export]
macro_rules! log_error {
($($arg:tt)*) => {
$crate::Logger::global().error(&format!($($arg)*))
};
}
fn main() {
// Initialize with custom configuration
let _ = Logger::init(
LogLevel::Debug,
vec![
Arc::new(ConsoleTarget::new(true)),
// Arc::new(FileTarget::new("app.log").unwrap()),
],
);
let logger = Logger::global();
logger.info("Application started");
logger.debug("Debug information");
logger.warn("Warning message");
logger.error("Error occurred!");
// Using macros
log_info!("User {} logged in", "alice");
log_error!("Failed to process request: {}", "timeout");
}
A thread-safe database connection pool.
use std::sync::{OnceLock, Mutex, Condvar, Arc};
use std::collections::VecDeque;
use std::time::{Duration, Instant};
static POOL: OnceLock<ConnectionPool> = OnceLock::new();
#[derive(Debug)]
pub struct Connection {
id: u64,
created_at: Instant,
last_used: Instant,
}
impl Connection {
fn new(id: u64) -> Self {
let now = Instant::now();
Self {
id,
created_at: now,
last_used: now,
}
}
pub fn execute(&mut self, query: &str) -> Result<Vec<String>, PoolError> {
self.last_used = Instant::now();
// Simulate query execution
println!("Connection {} executing: {}", self.id, query);
Ok(vec!["result".to_string()])
}
fn is_healthy(&self) -> bool {
// Check if connection is still valid
self.last_used.elapsed() < Duration::from_secs(300)
}
}
#[derive(Debug)]
pub enum PoolError {
Timeout,
PoolExhausted,
ConnectionFailed(String),
}
impl std::fmt::Display for PoolError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PoolError::Timeout => write!(f, "Connection acquisition timed out"),
PoolError::PoolExhausted => write!(f, "Connection pool exhausted"),
PoolError::ConnectionFailed(msg) => write!(f, "Connection failed: {}", msg),
}
}
}
impl std::error::Error for PoolError {}
pub struct PoolConfig {
pub min_connections: usize,
pub max_connections: usize,
pub connection_timeout: Duration,
pub idle_timeout: Duration,
}
impl Default for PoolConfig {
fn default() -> Self {
Self {
min_connections: 1,
max_connections: 10,
connection_timeout: Duration::from_secs(30),
idle_timeout: Duration::from_secs(600),
}
}
}
struct PoolInner {
connections: VecDeque<Connection>,
total_created: u64,
in_use: usize,
}
pub struct ConnectionPool {
config: PoolConfig,
inner: Mutex<PoolInner>,
available: Condvar,
}
impl ConnectionPool {
pub fn global() -> &'static ConnectionPool {
POOL.get_or_init(|| {
ConnectionPool::new(PoolConfig::default())
})
}
pub fn init(config: PoolConfig) -> Result<(), ConnectionPool> {
POOL.set(ConnectionPool::new(config))
}
fn new(config: PoolConfig) -> Self {
let mut connections = VecDeque::with_capacity(config.max_connections);
// Create minimum connections
for i in 0..config.min_connections {
connections.push_back(Connection::new(i as u64));
}
Self {
inner: Mutex::new(PoolInner {
connections,
total_created: config.min_connections as u64,
in_use: 0,
}),
available: Condvar::new(),
config,
}
}
pub fn acquire(&self) -> Result<PooledConnection<'_>, PoolError> {
let deadline = Instant::now() + self.config.connection_timeout;
let mut inner = self.inner.lock().unwrap();
loop {
// Try to get an existing connection
while let Some(mut conn) = inner.connections.pop_front() {
if conn.is_healthy() {
inner.in_use += 1;
return Ok(PooledConnection {
pool: self,
conn: Some(conn),
});
}
// Connection unhealthy, discard it
}
// Create new connection if under limit
let total = inner.connections.len() + inner.in_use;
if total < self.config.max_connections {
let id = inner.total_created;
inner.total_created += 1;
inner.in_use += 1;
return Ok(PooledConnection {
pool: self,
conn: Some(Connection::new(id)),
});
}
// Wait for a connection to become available
let timeout = deadline.saturating_duration_since(Instant::now());
if timeout.is_zero() {
return Err(PoolError::Timeout);
}
let (guard, timeout_result) = self.available
.wait_timeout(inner, timeout)
.unwrap();
inner = guard;
if timeout_result.timed_out() && inner.connections.is_empty() {
return Err(PoolError::Timeout);
}
}
}
fn return_connection(&self, conn: Connection) {
let mut inner = self.inner.lock().unwrap();
inner.in_use -= 1;
if conn.is_healthy() && inner.connections.len() < self.config.max_connections {
inner.connections.push_back(conn);
}
self.available.notify_one();
}
pub fn status(&self) -> PoolStatus {
let inner = self.inner.lock().unwrap();
PoolStatus {
available: inner.connections.len(),
in_use: inner.in_use,
total_created: inner.total_created,
}
}
}
#[derive(Debug)]
pub struct PoolStatus {
pub available: usize,
pub in_use: usize,
pub total_created: u64,
}
/// RAII guard that returns connection to pool on drop
pub struct PooledConnection<'a> {
pool: &'a ConnectionPool,
conn: Option<Connection>,
}
impl<'a> PooledConnection<'a> {
pub fn execute(&mut self, query: &str) -> Result<Vec<String>, PoolError> {
self.conn
.as_mut()
.ok_or(PoolError::ConnectionFailed("Connection consumed".into()))?
.execute(query)
}
}
impl<'a> Drop for PooledConnection<'a> {
fn drop(&mut self) {
if let Some(conn) = self.conn.take() {
self.pool.return_connection(conn);
}
}
}
fn main() -> Result<(), PoolError> {
// Initialize pool with custom config
let _ = ConnectionPool::init(PoolConfig {
min_connections: 2,
max_connections: 5,
connection_timeout: Duration::from_secs(10),
idle_timeout: Duration::from_secs(300),
});
let pool = ConnectionPool::global();
println!("Pool status: {:?}", pool.status());
// Acquire and use connections
{
let mut conn1 = pool.acquire()?;
let mut conn2 = pool.acquire()?;
conn1.execute("SELECT * FROM users")?;
conn2.execute("SELECT * FROM orders")?;
println!("Pool status (2 in use): {:?}", pool.status());
// Connections returned automatically when dropped
}
println!("Pool status (all returned): {:?}", pool.status());
Ok(())
}
A singleton service registry for dependency injection.
use std::sync::{OnceLock, RwLock};
use std::collections::HashMap;
use std::any::{Any, TypeId};
static REGISTRY: OnceLock<ServiceRegistry> = OnceLock::new();
pub struct ServiceRegistry {
services: RwLock<HashMap<TypeId, Box<dyn Any + Send + Sync>>>,
}
impl ServiceRegistry {
pub fn global() -> &'static ServiceRegistry {
REGISTRY.get_or_init(|| ServiceRegistry {
services: RwLock::new(HashMap::new()),
})
}
/// Register a service instance
pub fn register<T: 'static + Send + Sync>(&self, service: T) {
let type_id = TypeId::of::<T>();
let mut services = self.services.write().unwrap();
services.insert(type_id, Box::new(service));
}
/// Get a reference to a registered service
pub fn get<T: 'static + Send + Sync>(&self) -> Option<&T> {
let type_id = TypeId::of::<T>();
let services = self.services.read().unwrap();
services.get(&type_id).and_then(|boxed| {
// Safety: We know the type matches because we used TypeId as key
boxed.downcast_ref::<T>()
})
}
/// Check if a service is registered
pub fn has<T: 'static>(&self) -> bool {
let type_id = TypeId::of::<T>();
let services = self.services.read().unwrap();
services.contains_key(&type_id)
}
}
// Example services
pub trait EmailService: Send + Sync {
fn send(&self, to: &str, subject: &str, body: &str) -> Result<(), String>;
}
pub struct SmtpEmailService {
server: String,
}
impl SmtpEmailService {
pub fn new(server: &str) -> Self {
Self { server: server.to_string() }
}
}
impl EmailService for SmtpEmailService {
fn send(&self, to: &str, subject: &str, body: &str) -> Result<(), String> {
println!("Sending via {}: To={}, Subject={}", self.server, to, subject);
Ok(())
}
}
pub trait PaymentService: Send + Sync {
fn charge(&self, amount: f64, currency: &str) -> Result<String, String>;
}
pub struct StripePaymentService {
api_key: String,
}
impl StripePaymentService {
pub fn new(api_key: &str) -> Self {
Self { api_key: api_key.to_string() }
}
}
impl PaymentService for StripePaymentService {
fn charge(&self, amount: f64, currency: &str) -> Result<String, String> {
println!("Charging {} {} via Stripe", amount, currency);
Ok("ch_123456".to_string())
}
}
fn main() {
let registry = ServiceRegistry::global();
// Register services at startup
registry.register(SmtpEmailService::new("smtp.example.com"));
registry.register(StripePaymentService::new("sk_test_123"));
// Use services anywhere in the application
if let Some(email) = registry.get::<SmtpEmailService>() {
email.send("user@example.com", "Welcome!", "Thanks for signing up!").unwrap();
}
if let Some(payment) = registry.get::<StripePaymentService>() {
let charge_id = payment.charge(99.99, "USD").unwrap();
println!("Charge created: {}", charge_id);
}
}
| Approach | Thread-Safe | Lazy Init | Mutable | Use Case |
|----------|-------------|-----------|---------|----------|
| OnceLock (std) | Yes | Yes | No | Configuration, immutable singletons |
| OnceLock | Yes | Yes | Yes | Mutable shared state |
| lazy_static! | Yes | Yes | No | Pre-1.70 Rust compatibility |
| static mut | No | No | Yes | Never use (unsafe, UB-prone) |
| thread_local! | N/A | Yes | Yes | Per-thread singletons |
// DON'T: Use static mut - undefined behavior in concurrent code
static mut COUNTER: u64 = 0;
fn bad_increment() {
unsafe {
COUNTER += 1; // Data race! UB!
}
}
// DON'T: Block on async in sync context for singleton init
static ASYNC_SINGLETON: OnceLock<Data> = OnceLock::new();
fn bad_async_init() -> &'static Data {
ASYNC_SINGLETON.get_or_init(|| {
// This blocks the thread and can cause deadlocks
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async_init()) // Bad!
})
}
// DON'T: Overuse singletons - prefer dependency injection
struct UserService;
impl UserService {
fn get_user(&self, id: u64) -> User {
// Bad: hidden dependency on singleton
let db = DatabasePool::global();
let cache = CacheService::global();
// ...
}
}
// DO: Use explicit dependencies
struct UserService {
db: Arc<DatabasePool>,
cache: Arc<CacheService>,
}
impl UserService {
fn new(db: Arc<DatabasePool>, cache: Arc<CacheService>) -> Self {
Self { db, cache }
}
fn get_user(&self, id: u64) -> User {
// Dependencies are explicit and testable
// ...
}
}
OnceLock for modern Rust - It's in std since 1.70init() methods for test configuration| Operation | Complexity | Notes |
|-----------|------------|-------|
| First access | O(1) amortized | One-time initialization cost |
| Subsequent access | O(1) | Just pointer dereference |
| Memory | Single instance | Plus OnceLock overhead (~pointer size) |
| Contention | Low after init | Only first access may contend |
| Scenario | Use Instead |
|----------|-------------|
| Need testability | Dependency injection with traits |
| Per-request state | Request-scoped services |
| Per-thread state | thread_local! |
| Multiple environments | Configuration objects passed explicitly |
| Library code | Let users create instances |
Counter with increment() and get() methodsRandomGenerator seeded once at startupMetricsCollector singleton that tracks request counts and latenciesFeatureFlags singleton that can be updated at runtimePluginRegistry singleton that lazily loads pluginsCircuitBreaker singleton with state machine transitionsRun this code in the official Rust Playground