diff --git a/README.md b/README.md index 9cdceef..7fdff7e 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ A Rust procedural macro library for implementing thread-safe singleton patterns - **Thread-safe**: Uses `std::sync::OnceLock` for safe concurrent access - **Flexible return types**: Choose between `&'static Self` or `Arc` +- **Configurable sleep mechanism**: Automatically sleeps and retries when singleton is not yet initialized - **Simple API**: Just two methods - `init()` and `global()` - **Zero runtime overhead**: All safety guarantees are compile-time @@ -72,6 +73,24 @@ fn main() { } ``` +### Custom Sleep Duration + +```rust +use qsinglton::singleton; + +// Custom sleep duration of 1000ms +#[singleton(sleep_ms = 1000)] +struct Settings { + timeout: u64, +} + +// Arc with custom sleep duration of 300ms +#[singleton(arc, sleep_ms = 300)] +struct Cache { + data: Vec, +} +``` + ## API ### `init(instance: Self)` @@ -80,7 +99,16 @@ Initializes the singleton with the provided instance. Panics if called more than ### `global() -> &'static Self` or `global() -> &'static Arc` -Returns a reference to the global singleton instance. Panics if called before `init()`. +Returns a reference to the global singleton instance. If the singleton is not yet initialized, this method will sleep for the configured duration (default 500ms) and retry up to 20 times before panicking. + +This sleep mechanism is particularly useful in multi-threaded scenarios where one thread is initializing the singleton while another thread attempts to access it. + +## Sleep Configuration + +- **Default behavior**: `#[singleton]` - 500ms sleep intervals +- **Custom sleep**: `#[singleton(sleep_ms = 1000)]` - Custom duration in milliseconds +- **Arc with custom sleep**: `#[singleton(arc, sleep_ms = 300)]` - Arc mode with custom sleep +- **Maximum retries**: 20 attempts (configurable in generated code) ## Thread Safety @@ -89,6 +117,7 @@ All generated code is thread-safe and uses `std::sync::OnceLock` internally to e - Only one initialization can succeed - Multiple threads can safely access the singleton - No data races or undefined behavior +- Graceful handling of initialization delays through configurable sleep mechanism ## License diff --git a/examples/parameter_test.rs b/examples/parameter_test.rs new file mode 100644 index 0000000..3c2fdfe --- /dev/null +++ b/examples/parameter_test.rs @@ -0,0 +1,56 @@ +use qsingleton::singleton; + +// Test various parameter combinations + +// Just sleep_ms parameter +#[singleton(sleep_ms = 250)] +#[derive(Debug)] +struct OnlySleep { + data: i32, +} + +// Arc with sleep_ms +#[singleton(arc, sleep_ms = 750)] +#[derive(Debug)] +struct ArcWithSleep { + data: String, +} + +// Default parameters (should use 500ms) +#[singleton] +#[derive(Debug)] +struct DefaultParams { + data: bool, +} + +// Just arc (should use 500ms default) +#[singleton(arc)] +#[derive(Debug)] +struct JustArc { + data: f64, +} + +fn main() { + println!("Testing parameter parsing combinations"); + + // Initialize all singletons + OnlySleep::init(OnlySleep { data: 1 }); + ArcWithSleep::init(ArcWithSleep { data: "test".to_string() }); + DefaultParams::init(DefaultParams { data: true }); + JustArc::init(JustArc { data: 3.14 }); + + // Access them + let only_sleep = OnlySleep::global(); + println!("OnlySleep: {}", only_sleep.data); + + let arc_with_sleep = ArcWithSleep::global(); + println!("ArcWithSleep: {}", arc_with_sleep.data); + + let default_params = DefaultParams::global(); + println!("DefaultParams: {}", default_params.data); + + let just_arc = JustArc::global(); + println!("JustArc: {}", just_arc.data); + + println!("All parameter combinations work correctly!"); +} \ No newline at end of file diff --git a/examples/simple_sleep.rs b/examples/simple_sleep.rs new file mode 100644 index 0000000..b3d0bad --- /dev/null +++ b/examples/simple_sleep.rs @@ -0,0 +1,45 @@ +use qsingleton::singleton; +use std::thread; +use std::time::Duration; + +// Basic singleton with default 500ms sleep +#[singleton] +#[derive(Debug)] +struct Config { + name: String, + version: String, +} + +fn main() { + println!("QSingleton Sleep Feature Example"); + println!("================================="); + + // Spawn a thread that tries to access the singleton before it's initialized + let config_thread = thread::spawn(|| { + println!("Thread: Trying to access Config singleton..."); + println!("Thread: This will sleep 500ms intervals until initialization completes"); + let config = Config::global(); + println!("Thread: Success! Got config: {} v{}", config.name, config.version); + }); + + // Simulate some initialization work + println!("Main: Doing some initialization work for 1.2 seconds..."); + thread::sleep(Duration::from_millis(1200)); + + // Initialize the singleton + println!("Main: Initializing Config singleton"); + Config::init(Config { + name: "MyApp".to_string(), + version: "2.0.0".to_string(), + }); + + // Wait for the thread to complete + config_thread.join().unwrap(); + + // Show that subsequent accesses are immediate + println!("Main: Accessing Config again (should be immediate)"); + let config = Config::global(); + println!("Main: Got config immediately: {} v{}", config.name, config.version); + + println!("\nExample completed successfully!"); +} \ No newline at end of file diff --git a/examples/sleep_demo.rs b/examples/sleep_demo.rs new file mode 100644 index 0000000..2556bbd --- /dev/null +++ b/examples/sleep_demo.rs @@ -0,0 +1,109 @@ +use qsingleton::singleton; +use std::thread; +use std::time::{Duration, Instant}; + +// Basic singleton with default 500ms sleep +#[singleton] +#[derive(Debug)] +struct DefaultSleep { + value: String, +} + +// Singleton with custom 1000ms sleep +#[singleton(sleep_ms = 1000)] +#[derive(Debug)] +struct CustomSleep { + value: String, +} + +// Arc-based singleton with custom 200ms sleep +#[singleton(arc, sleep_ms = 200)] +#[derive(Debug)] +struct ArcCustomSleep { + value: String, +} + +fn main() { + println!("Demonstrating sleep functionality in qsingleton"); + println!("=============================================="); + + // Test 1: Default sleep behavior (500ms) + println!("\n1. Testing default sleep (500ms):"); + + let handle1 = thread::spawn(|| { + println!("Thread 1: Attempting to access DefaultSleep (should sleep and wait)"); + let start = Instant::now(); + let instance = DefaultSleep::global(); + let elapsed = start.elapsed(); + println!("Thread 1: Got instance after {:?}: {}", elapsed, instance.value); + }); + + // Initialize after a delay to test sleep behavior + thread::sleep(Duration::from_millis(750)); + println!("Main: Initializing DefaultSleep after 750ms delay"); + DefaultSleep::init(DefaultSleep { + value: "Default sleep singleton initialized!".to_string(), + }); + + handle1.join().unwrap(); + + // Test 2: Custom sleep behavior (1000ms) + println!("\n2. Testing custom sleep (1000ms):"); + + let handle2 = thread::spawn(|| { + println!("Thread 2: Attempting to access CustomSleep (should sleep longer)"); + let start = Instant::now(); + let instance = CustomSleep::global(); + let elapsed = start.elapsed(); + println!("Thread 2: Got instance after {:?}: {}", elapsed, instance.value); + }); + + // Initialize after a delay to test custom sleep behavior + thread::sleep(Duration::from_millis(1500)); + println!("Main: Initializing CustomSleep after 1500ms delay"); + CustomSleep::init(CustomSleep { + value: "Custom sleep singleton initialized!".to_string(), + }); + + handle2.join().unwrap(); + + // Test 3: Arc with custom sleep (200ms) + println!("\n3. Testing Arc with custom sleep (200ms):"); + + let handle3 = thread::spawn(|| { + println!("Thread 3: Attempting to access ArcCustomSleep (fast sleep)"); + let start = Instant::now(); + let instance = ArcCustomSleep::global(); + let elapsed = start.elapsed(); + println!("Thread 3: Got Arc instance after {:?}: {}", elapsed, instance.value); + }); + + // Initialize after a short delay + thread::sleep(Duration::from_millis(300)); + println!("Main: Initializing ArcCustomSleep after 300ms delay"); + ArcCustomSleep::init(ArcCustomSleep { + value: "Arc custom sleep singleton initialized!".to_string(), + }); + + handle3.join().unwrap(); + + // Test 4: Multiple threads accessing the same initialized singleton + println!("\n4. Testing multiple threads accessing initialized singleton:"); + + let handles: Vec<_> = (0..3) + .map(|i| { + thread::spawn(move || { + let start = Instant::now(); + let instance = DefaultSleep::global(); + let elapsed = start.elapsed(); + println!("Thread {}: Got instance immediately after {:?}: {}", i, elapsed, instance.value); + }) + }) + .collect(); + + for handle in handles { + handle.join().unwrap(); + } + + println!("\nAll tests completed successfully!"); +} \ No newline at end of file diff --git a/examples/timeout_test.rs b/examples/timeout_test.rs new file mode 100644 index 0000000..4cbe755 --- /dev/null +++ b/examples/timeout_test.rs @@ -0,0 +1,48 @@ +use qsingleton::singleton; +use std::panic; +use std::time::Instant; + +#[singleton(sleep_ms = 100)] +#[derive(Debug)] +struct TimeoutTest { + data: String, +} + +fn main() { + println!("Testing timeout behavior..."); + + let result = panic::catch_unwind(|| { + let start = Instant::now(); + let _ = TimeoutTest::global(); // This should timeout and panic + start.elapsed() + }); + + match result { + Ok(_) => { + println!("ERROR: Expected timeout but got success!"); + std::process::exit(1); + } + Err(panic_info) => { + if let Some(message) = panic_info.downcast_ref::() { + println!("SUCCESS: Got expected panic message:"); + println!("{}", message); + + // Verify the message contains expected information + if message.contains("TimeoutTest") && + message.contains("not initialized") && + message.contains("20 retries") && + message.contains("100ms") { + println!("✓ Panic message contains all expected information"); + } else { + println!("✗ Panic message missing expected information"); + std::process::exit(1); + } + } else { + println!("ERROR: Panic payload is not a string"); + std::process::exit(1); + } + } + } + + println!("Timeout test completed successfully!"); +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 82ddfc4..f303cc8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,34 +1,75 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{parse::Parse, parse::ParseStream, parse_macro_input, Ident, ItemStruct}; +use syn::{parse::Parse, parse::ParseStream, parse_macro_input, Ident, ItemStruct, LitInt, Token}; struct SingletonArgs { use_arc: bool, + sleep_ms: Option, } impl Parse for SingletonArgs { fn parse(input: ParseStream) -> syn::Result { if input.is_empty() { - return Ok(SingletonArgs { use_arc: false }); + return Ok(SingletonArgs { + use_arc: false, + sleep_ms: Some(500), // Default 500ms sleep + }); } - let ident: Ident = input.parse()?; - if ident == "arc" { - Ok(SingletonArgs { use_arc: true }) + let mut use_arc = false; + let mut sleep_ms = Some(500u64); // Default value + + // Parse first argument + let first_ident: Ident = input.parse()?; + + if first_ident == "arc" { + use_arc = true; + + // Check if there's a comma for additional arguments + if input.peek(Token![,]) { + input.parse::()?; + + // Parse sleep_ms parameter + if !input.is_empty() { + let param_name: Ident = input.parse()?; + if param_name == "sleep_ms" { + input.parse::()?; + let sleep_value: LitInt = input.parse()?; + sleep_ms = Some(sleep_value.base10_parse()?); + } else { + return Err(syn::Error::new( + param_name.span(), + "Expected 'sleep_ms' parameter", + )); + } + } + } + } else if first_ident == "sleep_ms" { + // Parse sleep_ms parameter + input.parse::()?; + let sleep_value: LitInt = input.parse()?; + sleep_ms = Some(sleep_value.base10_parse()?); } else { - Err(syn::Error::new( - ident.span(), - "Expected 'arc' or no arguments", - )) + return Err(syn::Error::new( + first_ident.span(), + "Expected 'arc' or 'sleep_ms' parameter", + )); } + + Ok(SingletonArgs { use_arc, sleep_ms }) } } /// A macro to define a singleton struct. /// -/// There are two modes: -/// - Default(`#[singleton]`): Returns a static reference to `Self`. -/// - `#[singleton(arc)]`: Returns an `&'static Arc`, allowing for shared ownership across threads. +/// There are three modes: +/// - Default(`#[singleton]`): Returns a static reference to `Self` with 500ms default sleep. +/// - `#[singleton(arc)]`: Returns an `&'static Arc` with 500ms default sleep. +/// - `#[singleton(sleep_ms = 1000)]`: Custom sleep duration in milliseconds. +/// - `#[singleton(arc, sleep_ms = 1000)]`: Arc mode with custom sleep duration. +/// +/// The sleep mechanism waits for the specified duration when the singleton is not yet initialized, +/// allowing time for initialization to complete in multi-threaded scenarios. /// /// Usage: /// ``` @@ -47,6 +88,12 @@ impl Parse for SingletonArgs { /// connection_string: String, /// pool_size: usize, /// } +/// +/// #[singleton(sleep_ms = 1000)] +/// #[derive(Debug)] +/// struct CustomSleep { +/// data: String, +/// } /// ``` #[proc_macro_attribute] pub fn singleton(args: TokenStream, input: TokenStream) -> TokenStream { @@ -60,16 +107,36 @@ pub fn singleton(args: TokenStream, input: TokenStream) -> TokenStream { name.span(), ); + let sleep_duration_ms = args.sleep_ms.unwrap_or(500); + let impl_block = if args.use_arc { quote! { static #instance_name: ::std::sync::OnceLock<::std::sync::Arc<#name>> = ::std::sync::OnceLock::new(); impl #name { /// Get the global singleton instance as Arc + /// + /// This method will sleep for the configured duration if the singleton is not yet initialized, + /// allowing time for initialization to complete in concurrent scenarios. pub fn global() -> &'static ::std::sync::Arc { - #instance_name.get().expect(&format!( - "Singleton '{}' not initialized. Call init() first.", - stringify!(#name) - )) + const MAX_RETRIES: usize = 20; // Maximum 10 seconds with 500ms default sleep + let sleep_duration = ::std::time::Duration::from_millis(#sleep_duration_ms); + + for retry in 0..MAX_RETRIES { + if let Some(instance) = #instance_name.get() { + return instance; + } + + if retry < MAX_RETRIES - 1 { + ::std::thread::sleep(sleep_duration); + } + } + + panic!( + "Singleton '{}' not initialized after {} retries ({}ms each). Call init() first.", + stringify!(#name), + MAX_RETRIES, + #sleep_duration_ms + ) } /// Initialize the singleton instance @@ -86,11 +153,29 @@ pub fn singleton(args: TokenStream, input: TokenStream) -> TokenStream { static #instance_name: ::std::sync::OnceLock<#name> = ::std::sync::OnceLock::new(); impl #name { /// Get the global singleton instance + /// + /// This method will sleep for the configured duration if the singleton is not yet initialized, + /// allowing time for initialization to complete in concurrent scenarios. pub fn global() -> &'static Self { - #instance_name.get().expect(&format!( - "Singleton '{}' not initialized. Call init() first.", - stringify!(#name) - )) + const MAX_RETRIES: usize = 20; // Maximum 10 seconds with 500ms default sleep + let sleep_duration = ::std::time::Duration::from_millis(#sleep_duration_ms); + + for retry in 0..MAX_RETRIES { + if let Some(instance) = #instance_name.get() { + return instance; + } + + if retry < MAX_RETRIES - 1 { + ::std::thread::sleep(sleep_duration); + } + } + + panic!( + "Singleton '{}' not initialized after {} retries ({}ms each). Call init() first.", + stringify!(#name), + MAX_RETRIES, + #sleep_duration_ms + ) } /// Initialize the singleton instance diff --git a/tests/sleep_tests.rs b/tests/sleep_tests.rs new file mode 100644 index 0000000..8a5a28b --- /dev/null +++ b/tests/sleep_tests.rs @@ -0,0 +1,103 @@ +use qsingleton::singleton; +use std::thread; +use std::time::{Duration, Instant}; + +#[singleton] +#[derive(Debug)] +struct TestSingleton { + value: i32, +} + +#[singleton(sleep_ms = 100)] +#[derive(Debug)] +struct CustomSleepSingleton { + value: String, +} + +#[singleton(arc, sleep_ms = 50)] +#[derive(Debug)] +struct ArcSleepSingleton { + data: u64, +} + +#[test] +fn test_sleep_functionality() { + // Test default sleep behavior + let handle = thread::spawn(|| { + let start = Instant::now(); + let instance = TestSingleton::global(); + let elapsed = start.elapsed(); + assert_eq!(instance.value, 42); + // Should have waited at least one sleep cycle (500ms) + assert!(elapsed >= Duration::from_millis(400)); + }); + + // Initialize after a delay + thread::sleep(Duration::from_millis(600)); + TestSingleton::init(TestSingleton { value: 42 }); + + handle.join().unwrap(); +} + +#[test] +fn test_custom_sleep_duration() { + let handle = thread::spawn(|| { + let start = Instant::now(); + let instance = CustomSleepSingleton::global(); + let elapsed = start.elapsed(); + assert_eq!(instance.value, "test"); + // Should have waited at least one sleep cycle (100ms) + assert!(elapsed >= Duration::from_millis(80)); + }); + + // Initialize after a delay + thread::sleep(Duration::from_millis(150)); + CustomSleepSingleton::init(CustomSleepSingleton { + value: "test".to_string(), + }); + + handle.join().unwrap(); +} + +#[test] +fn test_arc_sleep_functionality() { + let handle = thread::spawn(|| { + let start = Instant::now(); + let instance = ArcSleepSingleton::global(); + let elapsed = start.elapsed(); + assert_eq!(instance.data, 123); + // Should have waited at least one sleep cycle (50ms) + assert!(elapsed >= Duration::from_millis(40)); + }); + + // Initialize after a delay + thread::sleep(Duration::from_millis(75)); + ArcSleepSingleton::init(ArcSleepSingleton { data: 123 }); + + handle.join().unwrap(); +} + +#[test] +fn test_immediate_access_after_init() { + // This test uses different singleton instances to avoid conflicts + + #[singleton] + #[derive(Debug)] + struct ImmediateSingleton { + name: String, + } + + // Initialize first + ImmediateSingleton::init(ImmediateSingleton { + name: "immediate".to_string(), + }); + + // Access should be immediate + let start = Instant::now(); + let instance = ImmediateSingleton::global(); + let elapsed = start.elapsed(); + + assert_eq!(instance.name, "immediate"); + // Should be nearly instant (less than 10ms) + assert!(elapsed < Duration::from_millis(10)); +} \ No newline at end of file