use redis::AsyncCommands as _;
use redis::Client;
use redis_lock::LockAcrossOptions;
use serial_test::serial;
use std::error::Error;
use std::process::Stdio;
#[cfg(feature = "sync")]
use redis::Commands as _;
#[cfg(feature = "sync")]
const ONE: &str = env!("CARGO_BIN_EXE_one");
#[cfg(feature = "sync")]
const TWO: &str = env!("CARGO_BIN_EXE_two");
const THREE: &str = env!("CARGO_BIN_EXE_three");
const FOUR: &str = env!("CARGO_BIN_EXE_four");
const FIVE: &str = env!("CARGO_BIN_EXE_five");
const REDIS_URL: &str = "redis://127.0.0.1/";
#[expect(
clippy::panic_in_result_fn,
reason = "It's annoying to handle the error here."
)]
#[expect(
clippy::tests_outside_test_module,
reason = "`#[serial]` break this lint"
)] #[test]
#[serial]
fn two() -> Result<(), Box<dyn Error>> {
tokio::runtime::Runtime::new()?.block_on(async {
const N: usize = 10;
let client = Client::open(REDIS_URL)?;
let mut conn = client.get_multiplexed_async_connection().await?;
redis::cmd("FLUSHALL").exec_async(&mut conn).await?;
conn.set::<_, _, ()>("account1", 1000i32).await?;
conn.set::<_, _, ()>("account2", 1000i32).await?;
conn.set::<_, _, ()>("account3", 1000i32).await?;
redis_lock::setup(&client).await?;
let threes = (0..N)
.map(|_| {
tokio::process::Command::new(THREE)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
})
.collect::<Result<Vec<_>, _>>()?;
let fours = (0..N)
.map(|_| {
tokio::process::Command::new(FOUR)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
})
.collect::<Result<Vec<_>, _>>()?;
let fives = (0..N)
.map(|_| {
tokio::process::Command::new(FIVE)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
})
.collect::<Result<Vec<_>, _>>()?;
for three in threes {
let output = three.wait_with_output().await?;
assert!(output.status.success());
assert!(output.stderr.is_empty());
}
for four in fours {
let output = four.wait_with_output().await?;
assert!(output.status.success());
assert!(output.stderr.is_empty());
}
for five in fives {
let output = five.wait_with_output().await?;
assert!(output.status.success());
assert!(output.stderr.is_empty());
}
let balance1: i64 = conn.get("account1").await?;
let balance2: i64 = conn.get("account2").await?;
let balance3: i64 = conn.get("account3").await?;
let total_balance = balance1
.checked_add(balance2)
.ok_or("overflow")?
.checked_add(balance3)
.ok_or("overflow")?;
if total_balance == 3000 {
Ok(())
} else {
Err("Total balance is not 3000".into())
}
})
}
#[expect(clippy::tests_outside_test_module, reason = "`#[serial]` breaks it")] #[cfg(feature = "sync")]
#[test]
#[serial]
fn one() -> Result<(), Box<dyn Error>> {
const N: usize = 10;
let client = Client::open(REDIS_URL)?;
let mut conn = client.get_connection()?;
redis::cmd("FLUSHALL").exec(&mut conn)?;
conn.set::<_, _, ()>("account1", 1000i32)?;
conn.set::<_, _, ()>("account2", 1000i32)?;
conn.set::<_, _, ()>("account3", 1000i32)?;
redis_lock::sync::setup(&client)?;
let ones = (0..N)
.map(|_| {
std::process::Command::new(ONE)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
})
.collect::<Result<Vec<_>, _>>()?;
let twos = (0..N)
.map(|_| {
std::process::Command::new(TWO)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
})
.collect::<Result<Vec<_>, _>>()?;
for one in ones {
let _output = one.wait_with_output()?;
}
for two in twos {
let _output = two.wait_with_output()?;
}
let balance1: i64 = conn.get("account1")?;
let balance2: i64 = conn.get("account2")?;
let balance3: i64 = conn.get("account3")?;
let total_balance = balance1
.checked_add(balance2)
.ok_or("overflow")?
.checked_add(balance3)
.ok_or("overflow")?;
if total_balance == 3000 {
Ok(())
} else {
Err("Total balance is not 3000".into())
}
}
#[expect(
clippy::panic_in_result_fn,
reason = "It's annoying to handle the error here."
)]
#[expect(
clippy::tests_outside_test_module,
reason = "`#[serial]` break this lint"
)] #[test]
#[serial]
fn single() -> Result<(), Box<dyn Error>> {
tokio::runtime::Runtime::new()?.block_on(async {
tracing_subscriber::fmt::fmt()
.with_max_level(tracing::level_filters::LevelFilter::WARN)
.init();
const N: usize = 100;
let client = Client::open(REDIS_URL)?;
let connection = std::sync::Arc::new(tokio::sync::Mutex::new(
client.get_multiplexed_async_connection().await?,
));
redis::cmd("FLUSHALL")
.exec_async(&mut *connection.lock().await)
.await?;
let mut x: usize = 0;
let ptr = std::ptr::from_mut::<usize>(&mut x) as usize;
let futures = (0..N)
.map(|_| {
let cconnection = connection.clone();
tokio::task::spawn(async move {
let _ = redis_lock::lock_across(
&[cconnection],
"resource",
async move {
let x: &mut usize = unsafe { &mut *(ptr as *mut usize) };
*x += 1;
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
},
LockAcrossOptions::default(),
)
.await;
})
})
.collect::<Vec<_>>();
for future in futures {
future.await?;
}
assert_eq!(x, N);
Ok(())
})
}