From 6b245809619a58ecfc06fc9abb9ca7e2f24432d9 Mon Sep 17 00:00:00 2001 From: Muhan Song Date: Fri, 29 Sep 2023 15:17:28 +0800 Subject: [PATCH] docs: add semaphore example Signed-off-by: Muhan Song --- tokio/Cargo.toml | 1 + tokio/src/sync/semaphore.rs | 114 ++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/tokio/Cargo.toml b/tokio/Cargo.toml index afdd74ea8d5..de9fa660b99 100644 --- a/tokio/Cargo.toml +++ b/tokio/Cargo.toml @@ -98,6 +98,7 @@ bytes = { version = "1.0.0", optional = true } mio = { version = "0.8.6", optional = true, default-features = false } num_cpus = { version = "1.8.0", optional = true } parking_lot = { version = "0.12.0", optional = true } +lazy_static = "1.4.0" [target.'cfg(not(target_family = "wasm"))'.dependencies] socket2 = { version = "0.5.3", optional = true, features = [ "all" ] } diff --git a/tokio/src/sync/semaphore.rs b/tokio/src/sync/semaphore.rs index 08a86f4b9f7..6e408cbec56 100644 --- a/tokio/src/sync/semaphore.rs +++ b/tokio/src/sync/semaphore.rs @@ -123,6 +123,120 @@ use std::sync::Arc; /// # } /// ``` /// +/// ## Prevent tests from running in parallel +/// +/// By default, Rust runs tests in the same file in parallel. However, in some cases, running two tests in parallel may lead to problems. +/// For example, this can happen when tests use the same database. +/// +/// Consider the following scenario: +/// 1. `test_insert`: Inserts a key-value pair into the database, then retrieves the value using the same key to verify the insertion. +/// 2. `test_update`: Inserts a key, then updates the key to a new value and verifies that the value has been accurately updated. +/// 3. `test_others`: A third test that doesn't modify the database state. It can run in parallel with the other tests. +/// +/// In this example, `test_insert` and `test_update` need to run in sequence to work, but it doesn't matter which test runs first. +/// We can leverage a semaphore with a single permit to address this challenge. +/// +/// ``` +/// use tokio::sync::{Semaphore, Mutex}; +/// use lazy_static::lazy_static; +/// # use std::collections::HashMap; +/// # struct Database { +/// # map: Mutex>, +/// # } +/// # impl Database { +/// # pub fn setup() -> Database { +/// # Database { +/// # map: Mutex::new(HashMap::new()), +/// # } +/// # } +/// # pub async fn insert(&self, key: &str, value: i32) { +/// # self.map.lock().await.insert(key.to_string(), value); +/// # } +/// # pub async fn update(&self, key: &str, value: i32) { +/// # self.map.lock().await +/// # .entry(key.to_string()) +/// # .and_modify(|origin| *origin = value); +/// # } +/// # pub async fn delete(&self, key: &str) { +/// # self.map.lock().await.remove(key); +/// # } +/// # pub async fn get(&self, key: &str) -> i32 { +/// # *self.map.lock().await.get(key).unwrap() +/// # } +/// # } +/// +/// // Initialize a static semaphore with only one permit, which is used to +/// // prevent test_insert and test_update from running in parallel. +/// static PERMIT: Semaphore = Semaphore::const_new(1); +/// +/// // Initialize the database that will be used by the subsequent tests. +/// lazy_static! { +/// static ref DB: Database = Database::setup(); +/// } +/// #[test] +/// # fn fake_test() {} +/// async fn test_insert() { +/// +/// // Acquire permit before proceeding. Since the semaphore has only one permit, +/// // the test will wait if the permit is already acquired by other tests. +/// let permit = PERMIT.acquire().await.unwrap(); +/// +/// // Do the actual test stuff with database +/// +/// // Insert a key-value pair to database +/// let (key, value) = ("name", 0); +/// DB.insert(key, value).await; +/// +/// // Verify if the value has been inserted correctly. +/// assert_eq!(DB.get(key).await, value); +/// +/// // Undo the insertation, make database status unchanged. +/// DB.delete(key).await; +/// +/// // Drop permit, make possible waiting test to run. +/// drop(permit); +/// } +/// #[test] +/// # fn fake_test() {} +/// async fn test_update() { +/// +/// // Acquire permit before proceeding. Since the semaphore has only one permit, +/// // the test will wait if the permit is already acquired by other tests. +/// let permit = PERMIT.acquire().await.unwrap(); +/// +/// // Do the same insert. +/// let (key, value) = ("name", 0); +/// DB.insert(key, value).await; +/// +/// // Update the existing value with a new one. +/// let new_value = 1; +/// DB.update(key, new_value).await; +/// +/// // Verify if the value has been updated correctly. +/// assert_eq!(DB.get(key).await, new_value); +/// +/// // Undo any modificattion. +/// DB.delete(key).await; +/// +/// // Drop permit, make possible waiting test to run. +/// drop(permit); +/// } +/// #[test] +/// # fn fake_test() {} +/// async fn test_others() { +/// // this test can run in parallel with test_insert and test_update. +/// // They have nothing to do with PERMIT. +/// } +/// +/// # #[tokio::main] +/// # async fn main() { +/// # test_insert().await; +/// # test_update().await; +/// # test_others().await; +/// # } +/// +/// ``` +/// /// ## Rate limiting using a token bucket /// /// Many applications and systems have constraints on the rate at which certain