mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-10-10 04:25:44 +08:00
Redis lookup backend implementation
This commit is contained in:
parent
78afc703f5
commit
b7869901ee
15 changed files with 586 additions and 6 deletions
78
Cargo.lock
generated
78
Cargo.lock
generated
|
@ -938,6 +938,20 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "combine"
|
||||||
|
version = "4.6.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"memchr",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "console"
|
name = "console"
|
||||||
version = "0.15.7"
|
version = "0.15.7"
|
||||||
|
@ -1000,6 +1014,12 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc16"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
|
@ -4347,6 +4367,37 @@ dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redis"
|
||||||
|
version = "0.24.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c580d9cbbe1d1b479e8d67cf9daf6a62c957e6846048408b80b43ac3f6af84cd"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
|
"combine",
|
||||||
|
"crc16",
|
||||||
|
"futures",
|
||||||
|
"futures-util",
|
||||||
|
"itoa",
|
||||||
|
"log",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rand",
|
||||||
|
"rustls 0.21.10",
|
||||||
|
"rustls-native-certs",
|
||||||
|
"rustls-pemfile 1.0.4",
|
||||||
|
"rustls-webpki 0.101.7",
|
||||||
|
"ryu",
|
||||||
|
"sha1_smol",
|
||||||
|
"socket2 0.4.10",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls 0.24.1",
|
||||||
|
"tokio-util",
|
||||||
|
"url",
|
||||||
|
"webpki-roots 0.23.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
@ -4833,6 +4884,16 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e7673e0aa20ee4937c6aacfc12bb8341cfbf054cdd21df6bec5fd0629fe9339b"
|
checksum = "e7673e0aa20ee4937c6aacfc12bb8341cfbf054cdd21df6bec5fd0629fe9339b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-webpki"
|
||||||
|
version = "0.100.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f6a5fc258f1c1276dfe3016516945546e2d5383911efc0fc4f1cdc5df3a4ae3"
|
||||||
|
dependencies = [
|
||||||
|
"ring 0.16.20",
|
||||||
|
"untrusted 0.7.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
version = "0.101.7"
|
version = "0.101.7"
|
||||||
|
@ -5168,6 +5229,12 @@ dependencies = [
|
||||||
"digest 0.10.7",
|
"digest 0.10.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1_smol"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1collisiondetection"
|
name = "sha1collisiondetection"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
|
@ -5467,6 +5534,7 @@ dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"blake3",
|
"blake3",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"deadpool",
|
||||||
"deadpool-postgres",
|
"deadpool-postgres",
|
||||||
"elasticsearch",
|
"elasticsearch",
|
||||||
"farmhash",
|
"farmhash",
|
||||||
|
@ -5483,6 +5551,7 @@ dependencies = [
|
||||||
"r2d2",
|
"r2d2",
|
||||||
"rand",
|
"rand",
|
||||||
"rayon",
|
"rayon",
|
||||||
|
"redis",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"ring 0.17.7",
|
"ring 0.17.7",
|
||||||
|
@ -6509,6 +6578,15 @@ dependencies = [
|
||||||
"webpki",
|
"webpki",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "0.23.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-webpki 0.100.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-roots"
|
name = "webpki-roots"
|
||||||
version = "0.25.3"
|
version = "0.25.3"
|
||||||
|
|
|
@ -31,8 +31,8 @@ tracing = "0.1"
|
||||||
jemallocator = "0.5.0"
|
jemallocator = "0.5.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
#default = ["sqlite", "foundationdb", "postgres", "mysql", "rocks", "elastic", "s3"]
|
#default = ["sqlite", "foundationdb", "postgres", "mysql", "rocks", "elastic", "s3", "redis"]
|
||||||
default = ["sqlite", "postgres", "mysql", "foundationdb", "rocks"]
|
default = ["sqlite", "postgres", "mysql", "redis"]
|
||||||
sqlite = ["store/sqlite"]
|
sqlite = ["store/sqlite"]
|
||||||
foundationdb = ["store/foundation"]
|
foundationdb = ["store/foundation"]
|
||||||
postgres = ["store/postgres"]
|
postgres = ["store/postgres"]
|
||||||
|
@ -40,3 +40,4 @@ mysql = ["store/mysql"]
|
||||||
rocks = ["store/rocks"]
|
rocks = ["store/rocks"]
|
||||||
elastic = ["store/elastic"]
|
elastic = ["store/elastic"]
|
||||||
s3 = ["store/s3"]
|
s3 = ["store/s3"]
|
||||||
|
redis = ["store/redis"]
|
||||||
|
|
|
@ -42,6 +42,8 @@ regex = "1.7.0"
|
||||||
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls-webpki-roots", "blocking"] }
|
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls-webpki-roots", "blocking"] }
|
||||||
flate2 = "1.0"
|
flate2 = "1.0"
|
||||||
async-trait = "0.1.68"
|
async-trait = "0.1.68"
|
||||||
|
redis = { version = "0.24.0", features = [ "tokio-comp", "tokio-rustls-comp", "tls-rustls-insecure", "tls-rustls-webpki-roots", "cluster-async"], optional = true }
|
||||||
|
deadpool = { version = "0.10.0", features = ["managed"], optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1.23", features = ["full"] }
|
tokio = { version = "1.23", features = ["full"] }
|
||||||
|
@ -55,6 +57,7 @@ mysql = ["mysql_async"]
|
||||||
s3 = ["rust-s3"]
|
s3 = ["rust-s3"]
|
||||||
foundation = ["foundationdb", "futures"]
|
foundation = ["foundationdb", "futures"]
|
||||||
fdb-chunked-bm = []
|
fdb-chunked-bm = []
|
||||||
|
redis = ["dep:redis", "deadpool"]
|
||||||
|
|
||||||
test_mode = []
|
test_mode = []
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,8 @@ pub mod memory;
|
||||||
pub mod mysql;
|
pub mod mysql;
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
pub mod postgres;
|
pub mod postgres;
|
||||||
|
#[cfg(feature = "redis")]
|
||||||
|
pub mod redis;
|
||||||
#[cfg(feature = "rocks")]
|
#[cfg(feature = "rocks")]
|
||||||
pub mod rocksdb;
|
pub mod rocksdb;
|
||||||
#[cfg(feature = "s3")]
|
#[cfg(feature = "s3")]
|
||||||
|
|
90
crates/store/src/backend/redis/lookup.rs
Normal file
90
crates/store/src/backend/redis/lookup.rs
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||||
|
*
|
||||||
|
* This file is part of Stalwart Mail Server.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of
|
||||||
|
* the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
* in the LICENSE file at the top-level directory of this distribution.
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* You can be released from the requirements of the AGPLv3 license by
|
||||||
|
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||||
|
* for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use redis::AsyncCommands;
|
||||||
|
|
||||||
|
use crate::{Deserialize, LookupKey, LookupValue};
|
||||||
|
|
||||||
|
use super::{RedisPool, RedisStore};
|
||||||
|
|
||||||
|
impl RedisStore {
|
||||||
|
pub async fn key_set(&self, key: Vec<u8>, value: LookupValue<Vec<u8>>) -> crate::Result<()> {
|
||||||
|
match &self.pool {
|
||||||
|
RedisPool::Single(pool) => self.key_set_(pool.get().await?.as_mut(), key, value).await,
|
||||||
|
RedisPool::Cluster(pool) => self.key_set_(pool.get().await?.as_mut(), key, value).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn key_get<T: Deserialize + std::fmt::Debug + 'static>(
|
||||||
|
&self,
|
||||||
|
key: LookupKey,
|
||||||
|
) -> crate::Result<LookupValue<T>> {
|
||||||
|
match &self.pool {
|
||||||
|
RedisPool::Single(pool) => self.key_get_(pool.get().await?.as_mut(), key).await,
|
||||||
|
RedisPool::Cluster(pool) => self.key_get_(pool.get().await?.as_mut(), key).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn key_get_<T: Deserialize + std::fmt::Debug + 'static>(
|
||||||
|
&self,
|
||||||
|
conn: &mut impl AsyncCommands,
|
||||||
|
key: LookupKey,
|
||||||
|
) -> crate::Result<LookupValue<T>> {
|
||||||
|
match key {
|
||||||
|
LookupKey::Key(key) => {
|
||||||
|
if let Some(value) = conn.get::<_, Option<Vec<u8>>>(key).await? {
|
||||||
|
T::deserialize(&value).map(|value| LookupValue::Value { value, expires: 0 })
|
||||||
|
} else {
|
||||||
|
Ok(LookupValue::None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LookupKey::Counter(key) => {
|
||||||
|
let value: Option<i64> = conn.get(key).await?;
|
||||||
|
Ok(LookupValue::Counter {
|
||||||
|
num: value.unwrap_or(0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn key_set_(
|
||||||
|
&self,
|
||||||
|
conn: &mut impl AsyncCommands,
|
||||||
|
key: Vec<u8>,
|
||||||
|
value: LookupValue<Vec<u8>>,
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
match value {
|
||||||
|
LookupValue::Value { value, expires } => {
|
||||||
|
if expires > 0 {
|
||||||
|
conn.set_ex(key, value, expires).await?;
|
||||||
|
} else {
|
||||||
|
conn.set(key, value).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LookupValue::Counter { num } => conn.incr(key, num).await?,
|
||||||
|
LookupValue::None => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
159
crates/store/src/backend/redis/mod.rs
Normal file
159
crates/store/src/backend/redis/mod.rs
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||||
|
*
|
||||||
|
* This file is part of Stalwart Mail Server.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of
|
||||||
|
* the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
* in the LICENSE file at the top-level directory of this distribution.
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* You can be released from the requirements of the AGPLv3 license by
|
||||||
|
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||||
|
* for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use deadpool::{
|
||||||
|
managed::{Manager, Pool, PoolError},
|
||||||
|
Runtime,
|
||||||
|
};
|
||||||
|
use redis::{
|
||||||
|
cluster::{ClusterClient, ClusterClientBuilder},
|
||||||
|
Client, RedisError,
|
||||||
|
};
|
||||||
|
use utils::config::{utils::AsKey, Config};
|
||||||
|
|
||||||
|
pub mod lookup;
|
||||||
|
pub mod pool;
|
||||||
|
|
||||||
|
pub struct RedisStore {
|
||||||
|
pool: RedisPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RedisConnectionManager {
|
||||||
|
client: Client,
|
||||||
|
timeout: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RedisClusterConnectionManager {
|
||||||
|
client: ClusterClient,
|
||||||
|
timeout: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RedisPool {
|
||||||
|
Single(Pool<RedisConnectionManager>),
|
||||||
|
Cluster(Pool<RedisClusterConnectionManager>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RedisStore {
|
||||||
|
pub async fn open(config: &Config, prefix: impl AsKey) -> crate::Result<Self> {
|
||||||
|
let prefix = prefix.as_key();
|
||||||
|
|
||||||
|
let db = if let Some(url) = config.value((&prefix, "url")) {
|
||||||
|
Self {
|
||||||
|
pool: RedisPool::Single(build_pool(
|
||||||
|
config,
|
||||||
|
&prefix,
|
||||||
|
RedisConnectionManager {
|
||||||
|
client: Client::open(url)?,
|
||||||
|
timeout: config.property_or_static((&prefix, "timeout"), "10s")?,
|
||||||
|
},
|
||||||
|
)?),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let addresses = config
|
||||||
|
.values((&prefix, "urls"))
|
||||||
|
.map(|(_, v)| v.to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if addresses.is_empty() {
|
||||||
|
return Err(crate::Error::InternalError(format!(
|
||||||
|
"No Redis cluster URLs specified for {prefix:?}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let mut builder = ClusterClientBuilder::new(addresses.into_iter());
|
||||||
|
if let Some(value) = config.property((&prefix, "username"))? {
|
||||||
|
builder = builder.username(value);
|
||||||
|
}
|
||||||
|
if let Some(value) = config.property((&prefix, "password"))? {
|
||||||
|
builder = builder.password(value);
|
||||||
|
}
|
||||||
|
if let Some(value) = config.property((&prefix, "retries"))? {
|
||||||
|
builder = builder.retries(value);
|
||||||
|
}
|
||||||
|
if let Some(value) = config.property::<Duration>((&prefix, "max-retry-wait"))? {
|
||||||
|
builder = builder.max_retry_wait(value.as_secs());
|
||||||
|
}
|
||||||
|
if let Some(value) = config.property::<Duration>((&prefix, "min-retry-wait"))? {
|
||||||
|
builder = builder.min_retry_wait(value.as_secs());
|
||||||
|
}
|
||||||
|
if let Some(true) = config.property::<bool>((&prefix, "read-from-replicas"))? {
|
||||||
|
builder = builder.read_from_replicas();
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
pool: RedisPool::Cluster(build_pool(
|
||||||
|
config,
|
||||||
|
&prefix,
|
||||||
|
RedisClusterConnectionManager {
|
||||||
|
client: builder.build()?,
|
||||||
|
timeout: config.property_or_static((&prefix, "timeout"), "10s")?,
|
||||||
|
},
|
||||||
|
)?),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_pool<M: Manager>(
|
||||||
|
config: &Config,
|
||||||
|
prefix: &str,
|
||||||
|
manager: M,
|
||||||
|
) -> utils::config::Result<Pool<M>> {
|
||||||
|
Pool::builder(manager)
|
||||||
|
.runtime(Runtime::Tokio1)
|
||||||
|
.max_size(config.property_or_static((prefix, "pool.max-connections"), "10")?)
|
||||||
|
.create_timeout(
|
||||||
|
config
|
||||||
|
.property_or_static::<Duration>((prefix, "pool.create-timeout"), "30s")?
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
.wait_timeout(config.property_or_static((prefix, "pool.wait-timeout"), "30s")?)
|
||||||
|
.recycle_timeout(config.property_or_static((prefix, "pool.recycle-timeout"), "30s")?)
|
||||||
|
.build()
|
||||||
|
.map_err(|err| {
|
||||||
|
format!(
|
||||||
|
"Failed to build pool for {prefix:?}: {err}",
|
||||||
|
prefix = prefix,
|
||||||
|
err = err
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PoolError<RedisError>> for crate::Error {
|
||||||
|
fn from(value: PoolError<RedisError>) -> Self {
|
||||||
|
crate::Error::InternalError(format!("Redis pool error: {}", value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PoolError<crate::Error>> for crate::Error {
|
||||||
|
fn from(value: PoolError<crate::Error>) -> Self {
|
||||||
|
crate::Error::InternalError(format!("Connection pool {}", value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RedisError> for crate::Error {
|
||||||
|
fn from(value: RedisError) -> Self {
|
||||||
|
crate::Error::InternalError(format!("Redis error: {}", value))
|
||||||
|
}
|
||||||
|
}
|
83
crates/store/src/backend/redis/pool.rs
Normal file
83
crates/store/src/backend/redis/pool.rs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||||
|
*
|
||||||
|
* This file is part of Stalwart Mail Server.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of
|
||||||
|
* the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
* in the LICENSE file at the top-level directory of this distribution.
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* You can be released from the requirements of the AGPLv3 license by
|
||||||
|
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||||
|
* for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use deadpool::managed;
|
||||||
|
use redis::{
|
||||||
|
aio::{Connection, ConnectionLike},
|
||||||
|
cluster_async::ClusterConnection,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{RedisClusterConnectionManager, RedisConnectionManager};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl managed::Manager for RedisConnectionManager {
|
||||||
|
type Type = Connection;
|
||||||
|
type Error = crate::Error;
|
||||||
|
|
||||||
|
async fn create(&self) -> Result<Connection, crate::Error> {
|
||||||
|
match tokio::time::timeout(self.timeout, self.client.get_tokio_connection()).await {
|
||||||
|
Ok(conn) => conn.map_err(Into::into),
|
||||||
|
Err(_) => Err(crate::Error::InternalError(
|
||||||
|
"Redis connection timeout".into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn recycle(
|
||||||
|
&self,
|
||||||
|
conn: &mut Connection,
|
||||||
|
_: &managed::Metrics,
|
||||||
|
) -> managed::RecycleResult<crate::Error> {
|
||||||
|
conn.req_packed_command(&redis::cmd("PING"))
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|err| managed::RecycleError::Backend(err.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl managed::Manager for RedisClusterConnectionManager {
|
||||||
|
type Type = ClusterConnection;
|
||||||
|
type Error = crate::Error;
|
||||||
|
|
||||||
|
async fn create(&self) -> Result<ClusterConnection, crate::Error> {
|
||||||
|
match tokio::time::timeout(self.timeout, self.client.get_async_connection()).await {
|
||||||
|
Ok(conn) => conn.map_err(Into::into),
|
||||||
|
Err(_) => Err(crate::Error::InternalError(
|
||||||
|
"Redis connection timeout".into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn recycle(
|
||||||
|
&self,
|
||||||
|
conn: &mut ClusterConnection,
|
||||||
|
_: &managed::Metrics,
|
||||||
|
) -> managed::RecycleResult<crate::Error> {
|
||||||
|
conn.req_packed_command(&redis::cmd("PING"))
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|err| managed::RecycleError::Backend(err.into()))
|
||||||
|
}
|
||||||
|
}
|
|
@ -53,6 +53,9 @@ use crate::backend::rocksdb::RocksDbStore;
|
||||||
#[cfg(feature = "elastic")]
|
#[cfg(feature = "elastic")]
|
||||||
use crate::backend::elastic::ElasticSearchStore;
|
use crate::backend::elastic::ElasticSearchStore;
|
||||||
|
|
||||||
|
#[cfg(feature = "redis")]
|
||||||
|
use crate::backend::redis::RedisStore;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait ConfigStore {
|
pub trait ConfigStore {
|
||||||
async fn parse_stores(&self) -> utils::config::Result<Stores>;
|
async fn parse_stores(&self) -> utils::config::Result<Stores>;
|
||||||
|
@ -161,6 +164,13 @@ impl ConfigStore for Config {
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "redis")]
|
||||||
|
"redis" => {
|
||||||
|
config
|
||||||
|
.lookup_stores
|
||||||
|
.insert(store_id, RedisStore::open(self, prefix).await?.into());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
"memory" => {
|
"memory" => {
|
||||||
let prefix = prefix.as_key();
|
let prefix = prefix.as_key();
|
||||||
for lookup_id in self.sub_keys((&prefix, "lookup")) {
|
for lookup_id in self.sub_keys((&prefix, "lookup")) {
|
||||||
|
|
|
@ -49,6 +49,10 @@ impl LookupStore {
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
LookupStore::Memory(store) => store.query(query, params),
|
LookupStore::Memory(store) => store.query(query, params),
|
||||||
|
#[cfg(feature = "redis")]
|
||||||
|
LookupStore::Redis(_) => Err(crate::Error::InternalError(
|
||||||
|
"Redis does not support queries".into(),
|
||||||
|
)),
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::trace!( context = "store", event = "query", query = query, result = ?result);
|
tracing::trace!( context = "store", event = "query", query = query, result = ?result);
|
||||||
|
@ -81,6 +85,8 @@ impl LookupStore {
|
||||||
batch.ops.push(Operation::Value { class, op });
|
batch.ops.push(Operation::Value { class, op });
|
||||||
store.write(batch.build()).await
|
store.write(batch.build()).await
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "redis")]
|
||||||
|
LookupStore::Redis(store) => store.key_set(key, value).await,
|
||||||
LookupStore::Memory(_) => unimplemented!(),
|
LookupStore::Memory(_) => unimplemented!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,6 +116,8 @@ impl LookupStore {
|
||||||
.await
|
.await
|
||||||
.map(|num| LookupValue::Counter { num }),
|
.map(|num| LookupValue::Counter { num }),
|
||||||
},
|
},
|
||||||
|
#[cfg(feature = "redis")]
|
||||||
|
LookupStore::Redis(store) => store.key_get(key).await,
|
||||||
LookupStore::Memory(_) => unimplemented!(),
|
LookupStore::Memory(_) => unimplemented!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -137,7 +145,7 @@ impl LookupStore {
|
||||||
store
|
store
|
||||||
.iterate(IterateParams::new(from_key, to_key), |key, value| {
|
.iterate(IterateParams::new(from_key, to_key), |key, value| {
|
||||||
if value.deserialize_be_u64(0)? < current_time {
|
if value.deserialize_be_u64(0)? < current_time {
|
||||||
expired_keys.push(key.to_vec());
|
expired_keys.push(key.get(1..).unwrap_or_default().to_vec());
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok(true)
|
||||||
})
|
})
|
||||||
|
@ -159,6 +167,8 @@ impl LookupStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "redis")]
|
||||||
|
LookupStore::Redis(store) => {}
|
||||||
LookupStore::Memory(_) => {}
|
LookupStore::Memory(_) => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -434,7 +434,7 @@ impl Store {
|
||||||
value
|
value
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
SUBSPACE_INDEX_VALUES if key[0] >= 2 => {
|
SUBSPACE_INDEX_VALUES if key[0] >= 3 => {
|
||||||
// Ignore named keys
|
// Ignore named keys
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,9 @@ use backend::rocksdb::RocksDbStore;
|
||||||
#[cfg(feature = "elastic")]
|
#[cfg(feature = "elastic")]
|
||||||
use backend::elastic::ElasticSearchStore;
|
use backend::elastic::ElasticSearchStore;
|
||||||
|
|
||||||
|
#[cfg(feature = "redis")]
|
||||||
|
use backend::redis::RedisStore;
|
||||||
|
|
||||||
pub trait Deserialize: Sized + Sync + Send {
|
pub trait Deserialize: Sized + Sync + Send {
|
||||||
fn deserialize(bytes: &[u8]) -> crate::Result<Self>;
|
fn deserialize(bytes: &[u8]) -> crate::Result<Self>;
|
||||||
}
|
}
|
||||||
|
@ -236,6 +239,8 @@ pub enum FtsStore {
|
||||||
pub enum LookupStore {
|
pub enum LookupStore {
|
||||||
Store(Store),
|
Store(Store),
|
||||||
Memory(Arc<MemoryStore>),
|
Memory(Arc<MemoryStore>),
|
||||||
|
#[cfg(feature = "redis")]
|
||||||
|
Redis(Arc<RedisStore>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
|
@ -293,6 +298,13 @@ impl From<ElasticSearchStore> for FtsStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "redis")]
|
||||||
|
impl From<RedisStore> for LookupStore {
|
||||||
|
fn from(store: RedisStore) -> Self {
|
||||||
|
Self::Redis(Arc::new(store))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Store> for FtsStore {
|
impl From<Store> for FtsStore {
|
||||||
fn from(store: Store) -> Self {
|
fn from(store: Store) -> Self {
|
||||||
Self::Store(store)
|
Self::Store(store)
|
||||||
|
|
|
@ -5,8 +5,8 @@ edition = "2021"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
#default = ["sqlite", "foundationdb", "postgres", "mysql", "rocks", "elastic", "s3"]
|
#default = ["sqlite", "foundationdb", "postgres", "mysql", "rocks", "elastic", "s3", "redis"]
|
||||||
default = ["sqlite", "postgres", "mysql", "foundationdb", "rocks"]
|
default = ["sqlite", "postgres", "mysql", "redis"]
|
||||||
sqlite = ["store/sqlite"]
|
sqlite = ["store/sqlite"]
|
||||||
foundationdb = ["store/foundation"]
|
foundationdb = ["store/foundation"]
|
||||||
postgres = ["store/postgres"]
|
postgres = ["store/postgres"]
|
||||||
|
@ -14,6 +14,7 @@ mysql = ["store/mysql"]
|
||||||
rocks = ["store/rocks"]
|
rocks = ["store/rocks"]
|
||||||
elastic = ["store/elastic"]
|
elastic = ["store/elastic"]
|
||||||
s3 = ["store/s3"]
|
s3 = ["store/s3"]
|
||||||
|
redis = ["store/redis"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
store = { path = "../crates/store", features = ["test_mode"] }
|
store = { path = "../crates/store", features = ["test_mode"] }
|
||||||
|
|
|
@ -46,6 +46,10 @@ duplicate-expiry = "7d"
|
||||||
type = "sqlite"
|
type = "sqlite"
|
||||||
path = "%PATH%/test_antispam.db"
|
path = "%PATH%/test_antispam.db"
|
||||||
|
|
||||||
|
#[store."redis"]
|
||||||
|
#type = "redis"
|
||||||
|
#url = "redis://127.0.0.1"
|
||||||
|
|
||||||
[store."default"]
|
[store."default"]
|
||||||
type = "memory"
|
type = "memory"
|
||||||
|
|
||||||
|
|
121
tests/src/store/lookup.rs
Normal file
121
tests/src/store/lookup.rs
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023, Stalwart Labs Ltd.
|
||||||
|
*
|
||||||
|
* This file is part of the Stalwart Mail Server.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of
|
||||||
|
* the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
* in the LICENSE file at the top-level directory of this distribution.
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* You can be released from the requirements of the AGPLv3 license by
|
||||||
|
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||||
|
* for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use store::{config::ConfigStore, LookupKey, LookupStore, LookupValue};
|
||||||
|
use utils::config::Config;
|
||||||
|
|
||||||
|
use crate::store::{TempDir, CONFIG};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
pub async fn lookup_tests() {
|
||||||
|
let temp_dir = TempDir::new("lookup_tests", true);
|
||||||
|
let config =
|
||||||
|
Config::new(&CONFIG.replace("{TMP}", temp_dir.path.as_path().to_str().unwrap())).unwrap();
|
||||||
|
let stores = config.parse_stores().await.unwrap();
|
||||||
|
|
||||||
|
for (store_id, store) in stores.lookup_stores {
|
||||||
|
println!("Testing lookup store {}...", store_id);
|
||||||
|
if let LookupStore::Store(store) = &store {
|
||||||
|
store.destroy().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test value expiry
|
||||||
|
let key = "xyz".as_bytes().to_vec();
|
||||||
|
assert_eq!(
|
||||||
|
LookupValue::None,
|
||||||
|
store
|
||||||
|
.key_get::<String>(LookupKey::Key(key.clone()))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
store
|
||||||
|
.key_set(
|
||||||
|
key.clone(),
|
||||||
|
LookupValue::Value {
|
||||||
|
value: "hello".to_string().into_bytes(),
|
||||||
|
expires: 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(matches!(store
|
||||||
|
.key_get::<String>(LookupKey::Key(key.clone()))
|
||||||
|
.await
|
||||||
|
.unwrap(), LookupValue::Value { value,.. } if value == "hello"));
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||||
|
assert_eq!(
|
||||||
|
LookupValue::None,
|
||||||
|
store
|
||||||
|
.key_get::<String>(LookupKey::Key(key.clone()))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
store.purge_expired().await.unwrap();
|
||||||
|
if let LookupStore::Store(store) = &store {
|
||||||
|
store.assert_is_empty(store.clone().into()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test key
|
||||||
|
store
|
||||||
|
.key_set(
|
||||||
|
key.clone(),
|
||||||
|
LookupValue::Value {
|
||||||
|
value: "world".to_string().into_bytes(),
|
||||||
|
expires: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
store.purge_expired().await.unwrap();
|
||||||
|
assert!(matches!(store
|
||||||
|
.key_get::<String>(LookupKey::Key(key.clone()))
|
||||||
|
.await
|
||||||
|
.unwrap(), LookupValue::Value { value,.. } if value == "world"));
|
||||||
|
|
||||||
|
// Test counter
|
||||||
|
let key = "abc".as_bytes().to_vec();
|
||||||
|
store
|
||||||
|
.key_set(key.clone(), LookupValue::Counter { num: 1 })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
LookupValue::Counter { num: 1 },
|
||||||
|
store
|
||||||
|
.key_get::<String>(LookupKey::Counter(key.clone()))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
store
|
||||||
|
.key_set(key.clone(), LookupValue::Counter { num: 2 })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
LookupValue::Counter { num: 3 },
|
||||||
|
store
|
||||||
|
.key_get::<String>(LookupKey::Counter(key.clone()))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,6 +23,7 @@
|
||||||
|
|
||||||
pub mod assign_id;
|
pub mod assign_id;
|
||||||
pub mod blob;
|
pub mod blob;
|
||||||
|
pub mod lookup;
|
||||||
pub mod ops;
|
pub mod ops;
|
||||||
pub mod query;
|
pub mod query;
|
||||||
|
|
||||||
|
@ -74,6 +75,11 @@ port = 3307
|
||||||
database = "stalwart"
|
database = "stalwart"
|
||||||
user = "root"
|
user = "root"
|
||||||
password = "password"
|
password = "password"
|
||||||
|
|
||||||
|
[store."redis"]
|
||||||
|
type = "redis"
|
||||||
|
url = "redis://127.0.0.1"
|
||||||
|
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
Loading…
Add table
Reference in a new issue