maybe-once
[](https://crates.io/crates/maybe-once)
[](https://docs.rs/maybe-once)

[](https://codecov.io/gh/ufoscout/maybe-once)
## What is this?
`maybe-once` offers a variation of `OnceLock` that keeps track of the number of references to the internal data
and drops it every time the references counter goes to 0.
## Why is this useful?
In Rust static variables are not dropped when the program terminates. This is a problem when you need to initialize a shared resource that must be dropped when it is no longer used or when the process terminates. This happens, for example, when you a have a common resource to be used by a set of integration tests, and you want it to be dropped when the tests terminates.
Check the examples to see how to use it with docker and [testcontainers](https://github.com/testcontainers/testcontainers-rs).
## Usage examples
```rust
mod test {
use std::sync::OnceLock;
use maybe_once::blocking::{Data, MaybeOnce};
/// A data initializer function. This can be called more than
/// once.
/// If everything goes as expected, it should only be called
/// once.
fn init() -> String {
// Expensive initialization logic here.
// For example, you can start here a docker container
// (e.g. by using testcontainers),
// when there will be no more references to the data,
// the data will be dropped and the container will be
// stopped.
"hello".to_string()
}
/// A function that holds a static reference to the `MaybeOnce`
/// object and returns a `Data` object.
pub fn data(serial: bool) -> Data<'static, String> {
static DATA: OnceLock<MaybeOnce<String>> = OnceLock::new();
DATA.get_or_init(|| MaybeOnce::new(|| init()))
.data(serial)
}
/// Here we have multiple tests that access the data.
/// As the tests are executed in parallel in multiple threads,
/// the internal counter of the `MaybeOnce` object will be
/// incremented to 3.
/// It will then decrement every time a test finishes,
/// and incrementes each time a new test starts.
/// Once the internal counter goes to 0, the data will be
/// dropped.
/// At this points all tests are (statistically) complete,
/// but if for some reason the data is accessed again, it will
/// be recreated using the `init` function.
///
/// WARNING: If you execute the tests with a single thread,
/// the data will be dropped after each test and recreated
/// each time.
#[test]
fn test1() {
let data = data(false);
println!("{}", *data);
}
#[test]
fn test2() {
let data = data(false);
println!("{}", *data);
}
#[test]
fn test3() {
let data = data(false);
println!("{}", *data);
}
}
```
## Usage with tokio
The `tokio` feature of this crate allows you to use the optional `MaybeOnceAsync` object to initialize a shared resource using an async function.
### Why `#[tokio_shared::test]` instead of `#[tokio::test]`?
`#[tokio::test]` spins up a **brand-new runtime for every test**. This causes issues that when you have multiple tests that need to access the same resource, the resource is dropped and recreated multiple times.
The `#[tokio_shared::test]` attribute (enabled by the `tokio` feature) fixes this by running all tests on a single, process-wide multi-threaded tokio runtime shared by every annotated test.
```rust
#[cfg(feature = "tokio")]
mod test {
use std::sync::OnceLock;
use maybe_once::tokio::{Data, MaybeOnceAsync};
use maybe_once::tokio_shared;
/// A data async initializer function.
async fn init() -> String {
// Expensive initialization logic here.
"hello".to_string()
}
/// A function that holds a static reference to the
/// `MaybeOnceAsync` object and returns a `Data`
/// object.
pub async fn data(serial: bool) -> Data<'static, String> {
static DATA: OnceLock<MaybeOnceAsync<String>> =
OnceLock::new();
DATA.get_or_init(||
MaybeOnceAsync::new(|| Box::pin(init()))
)
.data(serial)
.await
}
#[tokio_shared::test]
async fn test1() {
let data = data(false).await;
println!("{}", *data);
}
#[tokio_shared::test]
async fn test2() {
let data = data(false).await;
println!("{}", *data);
}
#[tokio_shared::test]
async fn test3() {
let data = data(false).await;
println!("{}", *data);
}
}
```