feat(labrinth): Redis Cluster (#6771)

* chore(labrinth): bump to redis 1.4.1

* feat(labrinth): redis cluster

* chore: cleanup

* feat(labrinth): cache locking

* fix(labrinth): clippy

* chore(labrinth): cleanup env, remove postcard support

* chore(ci): fix test env for labrinth

* chore(labrinth): bump all key versions

* chore(labrinth): improve redis key identities handling

* chore(labrinth): simplify deadline handling

* chore(labrinth): remove unused lease tracking

* chore(labrinth): remove distributed cache locking for now

* chore(labrinth): improve redis backend init error

* feat(labrinth): expose redis read replica strategy

* chore(ci): remove other connection mode tests

* chore: split xredis crate

* feat(xredis): primaries routing

* chore: tombi fmt

* chore: clippy

* chore: update query cache
This commit is contained in:
François-Xavier Talbot
2026-07-23 11:35:02 +02:00
committed by GitHub
parent 11af2651ef
commit b4d681e713
146 changed files with 4741 additions and 2000 deletions
+17
View File
@@ -0,0 +1,17 @@
mod local;
use std::time::Duration;
pub(super) use self::local::{LockAcquisition, LockCoordinator, LockWaiter};
pub(super) const WAIT_TIMEOUT: Duration = Duration::from_secs(5);
/// Normalize only the requested lookup form's case. Raw IDs and aliases remain
/// distinct lock identities and may therefore fill concurrently.
pub(super) fn normalize_key(key: &str, case_sensitive: bool) -> String {
if case_sensitive {
key.to_owned()
} else {
key.to_lowercase()
}
}
+121
View File
@@ -0,0 +1,121 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use dashmap::DashMap;
use dashmap::mapref::entry::Entry;
use tokio::sync::Notify;
use tokio::time::{Instant, timeout_at};
use crate::Error;
#[derive(Clone)]
pub(in crate::cache) struct LockCoordinator {
locks: Arc<DashMap<String, Arc<LockState>>>,
}
impl LockCoordinator {
pub(in crate::cache) fn new() -> Self {
Self {
locks: Arc::new(DashMap::with_capacity(2048)),
}
}
pub(in crate::cache) fn acquire(&self, key: String) -> LockAcquisition {
match self.locks.entry(key.clone()) {
Entry::Occupied(entry) => LockAcquisition::Waiting(LockWaiter {
state: entry.get().clone(),
}),
Entry::Vacant(entry) => {
let state = Arc::new(LockState::new());
entry.insert(state.clone());
LockAcquisition::Owned(OwnedLockGuard {
locks: self.locks.clone(),
key,
state,
released: false,
})
}
}
}
}
pub(in crate::cache) enum LockAcquisition {
Owned(OwnedLockGuard),
Waiting(LockWaiter),
}
pub(in crate::cache) struct OwnedLockGuard {
locks: Arc<DashMap<String, Arc<LockState>>>,
key: String,
state: Arc<LockState>,
released: bool,
}
impl OwnedLockGuard {
fn release_inner(&mut self) {
if self.released {
return;
}
self.released = true;
self.locks
.remove_if(&self.key, |_, state| Arc::ptr_eq(state, &self.state));
self.state.released.store(true, Ordering::Release);
self.state.notify.notify_waiters();
}
}
impl Drop for OwnedLockGuard {
fn drop(&mut self) {
self.release_inner();
}
}
pub(in crate::cache) struct LockWaiter {
state: Arc<LockState>,
}
impl LockWaiter {
pub(in crate::cache) async fn wait(
self,
deadline: Instant,
) -> Result<(), Error> {
loop {
if self.state.released.load(Ordering::Acquire) {
return Ok(());
}
let notified = self.state.notify.notified();
tokio::pin!(notified);
notified.as_mut().enable();
if self.state.released.load(Ordering::Acquire) {
return Ok(());
}
timeout_at(deadline, notified)
.await
.map_err(|_| lock_timeout())?;
}
}
}
struct LockState {
released: AtomicBool,
notify: Notify,
}
impl LockState {
fn new() -> Self {
Self {
released: AtomicBool::new(false),
notify: Notify::new(),
}
}
}
fn lock_timeout() -> Error {
Error::LocalCacheTimeout {
released: 0,
total: 1,
}
}