use crate::error::AedbError;
use parking_lot::{Condvar, Mutex};
use std::collections::{BTreeSet, HashMap};
use std::sync::Arc;
use std::time::{Duration, Instant};
pub const DEFAULT_LOCK_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct LockKey {
pub project_id: String,
pub scope_id: String,
pub object: Vec<u8>,
}
impl LockKey {
pub fn new(
project_id: impl Into<String>,
scope_id: impl Into<String>,
object: impl Into<Vec<u8>>,
) -> Self {
Self {
project_id: project_id.into(),
scope_id: scope_id.into(),
object: object.into(),
}
}
pub fn global(object: impl Into<Vec<u8>>) -> Self {
Self {
project_id: String::new(),
scope_id: String::new(),
object: object.into(),
}
}
}
impl From<&str> for LockKey {
fn from(s: &str) -> Self {
LockKey::global(s.as_bytes().to_vec())
}
}
impl From<String> for LockKey {
fn from(s: String) -> Self {
LockKey::global(s.into_bytes())
}
}
#[derive(Debug, Clone, Copy)]
struct HeldLock {
owner: u64,
fencing_token: u64,
}
#[derive(Default)]
struct Inner {
held: HashMap<LockKey, HeldLock>,
next_owner: u64,
next_fencing_token: u64,
}
pub struct LockManager {
inner: Mutex<Inner>,
released: Condvar,
default_timeout: Duration,
}
impl Default for LockManager {
fn default() -> Self {
Self::new(DEFAULT_LOCK_TIMEOUT)
}
}
impl LockManager {
pub fn new(default_timeout: Duration) -> Self {
Self {
inner: Mutex::new(Inner::default()),
released: Condvar::new(),
default_timeout,
}
}
pub fn begin(self: &Arc<Self>) -> TxLockSet {
let owner = {
let mut inner = self.inner.lock();
inner.next_owner += 1;
inner.next_owner
};
TxLockSet {
manager: Arc::clone(self),
owner,
held: BTreeSet::new(),
released: false,
}
}
pub fn held_count(&self) -> usize {
self.inner.lock().held.len()
}
pub fn is_locked(&self, key: &LockKey) -> bool {
self.inner.lock().held.contains_key(key)
}
fn acquire(
&self,
owner: u64,
key: &LockKey,
deadline: Option<Instant>,
) -> Result<u64, LockError> {
let mut inner = self.inner.lock();
loop {
match inner.held.get(key) {
Some(existing) if existing.owner == owner => {
return Ok(existing.fencing_token);
}
Some(_) => match deadline {
None => return Err(LockError::WouldBlock),
Some(deadline) => {
let now = Instant::now();
if now >= deadline {
return Err(LockError::Timeout);
}
if self
.released
.wait_for(&mut inner, deadline - now)
.timed_out()
&& Instant::now() >= deadline
{
return Err(LockError::Timeout);
}
}
},
None => {
inner.next_fencing_token += 1;
let fencing_token = inner.next_fencing_token;
inner.held.insert(
key.clone(),
HeldLock {
owner,
fencing_token,
},
);
return Ok(fencing_token);
}
}
}
}
fn release_one(&self, owner: u64, key: &LockKey) {
let mut inner = self.inner.lock();
if let Some(existing) = inner.held.get(key)
&& existing.owner == owner
{
inner.held.remove(key);
drop(inner);
self.released.notify_all();
}
}
fn release_many(&self, owner: u64, keys: &BTreeSet<LockKey>) {
if keys.is_empty() {
return;
}
{
let mut inner = self.inner.lock();
for key in keys {
if let Some(existing) = inner.held.get(key)
&& existing.owner == owner
{
inner.held.remove(key);
}
}
}
self.released.notify_all();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LockError {
WouldBlock,
Timeout,
}
pub struct TxLockSet {
manager: Arc<LockManager>,
owner: u64,
held: BTreeSet<LockKey>,
released: bool,
}
impl TxLockSet {
pub fn lock(&mut self, key: impl Into<LockKey>) -> Result<u64, AedbError> {
let timeout = self.manager.default_timeout;
self.lock_with_timeout(key, timeout)
}
pub fn lock_with_timeout(
&mut self,
key: impl Into<LockKey>,
timeout: Duration,
) -> Result<u64, AedbError> {
let key = key.into();
let deadline = Instant::now().checked_add(timeout);
let token = self
.manager
.acquire(self.owner, &key, deadline.or(Some(Instant::now())))
.map_err(|e| e.into_aedb(&key))?;
self.held.insert(key);
Ok(token)
}
pub fn try_lock(&mut self, key: impl Into<LockKey>) -> Result<Option<u64>, AedbError> {
let key = key.into();
match self.manager.acquire(self.owner, &key, None) {
Ok(token) => {
self.held.insert(key);
Ok(Some(token))
}
Err(LockError::WouldBlock) => Ok(None),
Err(e) => Err(e.into_aedb(&key)),
}
}
pub fn lock_all<K, I>(&mut self, keys: I) -> Result<(), AedbError>
where
K: Into<LockKey>,
I: IntoIterator<Item = K>,
{
let timeout = self.manager.default_timeout;
self.lock_all_with_timeout(keys, timeout)
}
pub fn lock_all_with_timeout<K, I>(
&mut self,
keys: I,
timeout: Duration,
) -> Result<(), AedbError>
where
K: Into<LockKey>,
I: IntoIterator<Item = K>,
{
let ordered: BTreeSet<LockKey> = keys.into_iter().map(Into::into).collect();
let deadline = Instant::now().checked_add(timeout).or(Some(Instant::now()));
let mut newly_acquired: BTreeSet<LockKey> = BTreeSet::new();
for key in &ordered {
if self.held.contains(key) {
continue;
}
match self.manager.acquire(self.owner, key, deadline) {
Ok(_) => {
newly_acquired.insert(key.clone());
}
Err(e) => {
self.manager.release_many(self.owner, &newly_acquired);
return Err(e.into_aedb(key));
}
}
}
self.held.extend(newly_acquired);
Ok(())
}
pub fn holds(&self, key: &LockKey) -> bool {
self.held.contains(key)
}
pub fn held_keys(&self) -> impl Iterator<Item = &LockKey> {
self.held.iter()
}
pub fn owner_id(&self) -> u64 {
self.owner
}
pub fn unlock(&mut self, key: &LockKey) {
if self.held.remove(key) {
self.manager.release_one(self.owner, key);
}
}
pub fn commit(mut self) {
self.release_all();
}
pub fn rollback(mut self) {
self.release_all();
}
fn release_all(&mut self) {
if self.released {
return;
}
self.released = true;
let keys = std::mem::take(&mut self.held);
self.manager.release_many(self.owner, &keys);
}
}
impl Drop for TxLockSet {
fn drop(&mut self) {
self.release_all();
}
}
impl LockError {
fn into_aedb(self, key: &LockKey) -> AedbError {
match self {
LockError::Timeout => AedbError::Timeout,
LockError::WouldBlock => AedbError::Conflict(format!(
"object lock held by another transaction: {}",
describe_key(key)
)),
}
}
}
fn describe_key(key: &LockKey) -> String {
let object = String::from_utf8(key.object.clone())
.unwrap_or_else(|_| format!("0x{}", hex::encode(&key.object)));
if key.project_id.is_empty() && key.scope_id.is_empty() {
object
} else {
format!("{}/{}/{}", key.project_id, key.scope_id, object)
}
}
#[cfg(test)]
#[path = "locks_tests.rs"]
mod locks_tests;