Skip to content

Commit

Permalink
Adds RwLock type (#14)
Browse files Browse the repository at this point in the history
This implements a RwLock type, which is very equal to the std::sync
equivalent.
  • Loading branch information
Heiss authored Oct 29, 2023
1 parent 28fdc2b commit c646c6e
Show file tree
Hide file tree
Showing 11 changed files with 509 additions and 7 deletions.
5 changes: 2 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
[package]
name = "micro_types"
version = "0.2.2"
version = "0.2.3"
edition = "2021"
readme = "README.md"
license = "MIT"
license-file = "LICENSE.md"
description = "Types for distributed systems"
repository = "https://github.com/rust-micro/types"
homepage = "https://github.com/rust-micro/types"
keywords = ["micro", "distributed", "type", "redis"]
categories = ["microservice", "type", "database"]
categories = ["network-programming", "data-structures", "database"]
documentation = "https://docs.rs/micro_types"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand Down
2 changes: 1 addition & 1 deletion src/redis/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub struct Generic<T> {

impl<T> Generic<T>
where
T: Display + Serialize + DeserializeOwned,
T: Serialize + DeserializeOwned,
{
/// The new method creates a new instance of the type.
/// It does not load or store any value in Redis.
Expand Down
2 changes: 1 addition & 1 deletion src/redis/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ where
{
/// Creates a new List
///
/// There is no `with_value` method like [Generic::with_value] because it is not possible to
/// There is no `with_value` method like (Generic::with_value)[crate::redis::Generic::with_value] because it is not possible to
/// provide a good default behaviour in redis. So you have to think about, how you want to handle
/// already stored values in redis.
/// If you want a small performance boost, look at [ListCache].
Expand Down
7 changes: 7 additions & 0 deletions src/redis/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
//! # Upcoming Features
//!
//! It will be possible to create happens-before relationships between store and load operations like atomic types.
//! Also it will be possible to create other backends than Redis.
//!
//! # Usage
//!
Expand All @@ -36,13 +37,18 @@
//!
//! More examples can be found on the doc pages of the types.
//!
//! # Custom Types
//!
//! It is possible to implement your own complex types by implementing the [BackedType](crate::BackedType) trait.
//! But it should not be needed as long as your type implements some or all of the various [Ops](https://doc.rust-lang.org/std/ops/index.html) traits.
mod bool_type;
mod clock;
mod generic;
mod helper;
mod integer;
mod list;
mod mutex;
mod rwlock;
mod string;

pub(crate) use helper::apply_operator;
Expand All @@ -56,4 +62,5 @@ pub use integer::{
};
pub use list::{List, ListCache, ListIter};
pub use mutex::{Guard, LockError, Mutex};
pub use rwlock::RwLock;
pub use string::TString as DString;
2 changes: 0 additions & 2 deletions src/redis/mutex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,6 @@ where
type Target = Generic<T>;

fn deref(&self) -> &Self::Target {
// Safety: The very existence of this Guard guarantees that we have exclusive access to the data.
&self.lock.data
}
}
Expand All @@ -350,7 +349,6 @@ where
T: DeserializeOwned + Serialize,
{
fn deref_mut(&mut self) -> &mut Self::Target {
// Safety: The very existence of this Guard guarantees that we have exclusive access to the data.
&mut self.lock.data
}
}
Expand Down
118 changes: 118 additions & 0 deletions src/redis/rwlock/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/// The read lock script.
///
/// Checks if the writer list besides the key is empty or the lock is set.
/// If there are no writers, the uuid will be set as a reader and returns true.
/// Returns false otherwise.
///
/// The timeout will be used for the reader lock. You need to retry to get the lock again if you want to keep it.
/// But if a writer comes in scope, the reader lock will be dropped after the timeout and you have to wait.
///
/// Takes 3 arguments:
/// 1. The key to lock
/// 2. The uuid of the lock
/// 3. The timeout in seconds
pub const READER_LOCK: &str = r#"
if redis.call("exists", ARGV[1] .. ":lock") == 1 then
return 0
end
local res = redis.call("scan", 0, "match", ARGV[1] .. ":writer_waiting_list:*")
if next(res[2]) == nil then
redis.call("set", ARGV[1] .. ":reader_locks:" .. ARGV[2], 1, "ex", ARGV[3])
return 1
end
return 0
"#;

/// The read lock drop script.
///
/// Removes the uuid from the reader list.
///
/// Takes 2 arguments:
/// 1. The key to lock
/// 2. The uuid of the lock
pub const READER_LOCK_DROP: &str = r#"
redis.call("del", ARGV[1] .. ":reader_locks:" .. ARGV[2])
return 1
"#;

/// The writer lock script.
///
/// Checks if the reader list besides the key is empty.
/// Also add the uuid to the writer waiting list.
/// If there are no readers, the uuid will be set as the lock and returns true.
/// Returns false otherwise.
///
/// The timeout will also be used for the waiting ticket, so if you wait too long, your intention will be dropped and reader can be acquired.
/// So be sure to request the lock again fast enough.
///
/// Takes 3 arguments:
/// 1. The key to lock
/// 2. The uuid of the lock
/// 3. The timeout in seconds for waiting
// TODO: Should lock be expanded, if there is already another writer waiting?
pub const WRITER_LOCK: &str = r#"
redis.call("set", ARGV[1] .. ":writer_waiting_list:" .. ARGV[2], 1, "ex", ARGV[3])
if redis.call("exists", ARGV[1] .. ":lock") == 1 then
return 0
end
return redis.call("set", ARGV[1] .. ":lock", ARGV[2], "nx", "ex", ARGV[3])
"#;

/// The writer lock drop script.
///
/// Removes the uuid from the writer list.
///
/// Takes 2 arguments:
/// 1. The key to lock
/// 2. The uuid of the lock
pub const WRITER_LOCK_DROP: &str = r#"
redis.call("del", ARGV[1] .. ":writer_waiting_list:" .. ARGV[2])
if redis.call("get", ARGV[1] .. ":lock") == ARGV[2] then
redis.call("del", ARGV[1] .. ":lock")
end
return 1
"#;

/// The uuid script.
///
/// Increments the uuid counter and returns the new value.
///
/// Takes 1 argument:
/// 1. The key to lock
pub const UUID_SCRIPT: &str = r#"
return redis.call("INCR", ARGV[1] .. ":lock_counter")
"#;

/// The read script.
///
/// Reads the value from the key, only if the uuid is in reader list or if the lock is equal to uuid.
///
/// Takes 2 argument:
/// 1. The key to read
/// 2. The uuid of the lock
pub const LOAD_SCRIPT: &str = r#"
if redis.call("get", ARGV[1] .. ":lock") == ARGV[2] then
return redis.call("get", ARGV[1])
end
if redis.call("exists", ARGV[1] .. ":reader_locks:" .. ARGV[2]) then
return redis.call("get", ARGV[1])
end
"#;

/// The store script.
///
/// Stores the value to the key, only if the uuid is in lock.
///
/// Takes 3 arguments:
/// 1. The key to store
/// 2. The uuid of the lock
/// 3. The value to store
pub const STORE_SCRIPT: &str = r#"
if redis.call("get", ARGV[1] .. ":lock") == ARGV[2] then
redis.call("set", ARGV[1], ARGV[3])
return 1
end
return 0
"#;
12 changes: 12 additions & 0 deletions src/redis/rwlock/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum RwLockError {
#[error("The lock is already locked for writer.")]
WriterAlreadyLocked,
#[error("The lock is already locked for reader.")]
StillReader,
#[error("The lock could not be dropped.")]
LockNotDroppable,
#[error("The lock is expired. Failed UUID: {0} ")]
LockExpired(usize),
}
Loading

0 comments on commit c646c6e

Please sign in to comment.