mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-11-07 19:37:35 +08:00
Refresh old FoundationDB read transactions (closes #520)
This commit is contained in:
parent
a9cffb8d1e
commit
b53c3d120e
4 changed files with 178 additions and 25 deletions
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use foundationdb::{api::NetworkAutoStop, Database, FdbError};
|
use foundationdb::{api::NetworkAutoStop, Database, FdbError, Transaction};
|
||||||
|
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
||||||
|
|
@ -16,6 +16,8 @@ pub mod read;
|
||||||
pub mod write;
|
pub mod write;
|
||||||
|
|
||||||
const MAX_VALUE_SIZE: usize = 100000;
|
const MAX_VALUE_SIZE: usize = 100000;
|
||||||
|
pub const TRANSACTION_EXPIRY: Duration = Duration::from_secs(1);
|
||||||
|
pub const TRANSACTION_TIMEOUT: Duration = Duration::from_secs(4);
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct FdbStore {
|
pub struct FdbStore {
|
||||||
|
|
@ -24,6 +26,11 @@ pub struct FdbStore {
|
||||||
version: parking_lot::Mutex<ReadVersion>,
|
version: parking_lot::Mutex<ReadVersion>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) struct TimedTransaction {
|
||||||
|
trx: Transaction,
|
||||||
|
expires: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct ReadVersion {
|
pub(crate) struct ReadVersion {
|
||||||
version: i64,
|
version: i64,
|
||||||
expires: Instant,
|
expires: Instant,
|
||||||
|
|
@ -33,7 +40,7 @@ impl ReadVersion {
|
||||||
pub fn new(version: i64) -> Self {
|
pub fn new(version: i64) -> Self {
|
||||||
Self {
|
Self {
|
||||||
version,
|
version,
|
||||||
expires: Instant::now() + Duration::from_secs(1),
|
expires: Instant::now() + TRANSACTION_EXPIRY,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,6 +58,25 @@ impl Default for ReadVersion {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AsRef<Transaction> for TimedTransaction {
|
||||||
|
fn as_ref(&self) -> &Transaction {
|
||||||
|
&self.trx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimedTransaction {
|
||||||
|
pub fn new(trx: Transaction) -> Self {
|
||||||
|
Self {
|
||||||
|
trx,
|
||||||
|
expires: Instant::now() + TRANSACTION_TIMEOUT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_expired(&self) -> bool {
|
||||||
|
self.expires < Instant::now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<FdbError> for Error {
|
impl From<FdbError> for Error {
|
||||||
fn from(error: FdbError) -> Self {
|
fn from(error: FdbError) -> Self {
|
||||||
Self::InternalError(format!("FoundationDB error: {}", error.message()))
|
Self::InternalError(format!("FoundationDB error: {}", error.message()))
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ use crate::{
|
||||||
BitmapKey, Deserialize, IterateParams, Key, ValueKey, U32_LEN, WITH_SUBSPACE,
|
BitmapKey, Deserialize, IterateParams, Key, ValueKey, U32_LEN, WITH_SUBSPACE,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{FdbStore, ReadVersion, MAX_VALUE_SIZE};
|
use super::{FdbStore, ReadVersion, TimedTransaction, MAX_VALUE_SIZE};
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) enum ChunkedValue {
|
pub(crate) enum ChunkedValue {
|
||||||
|
|
@ -81,31 +81,69 @@ impl FdbStore {
|
||||||
params: IterateParams<T>,
|
params: IterateParams<T>,
|
||||||
mut cb: impl for<'x> FnMut(&'x [u8], &'x [u8]) -> crate::Result<bool> + Sync + Send,
|
mut cb: impl for<'x> FnMut(&'x [u8], &'x [u8]) -> crate::Result<bool> + Sync + Send,
|
||||||
) -> crate::Result<()> {
|
) -> crate::Result<()> {
|
||||||
let begin = params.begin.serialize(WITH_SUBSPACE);
|
let mut begin = params.begin.serialize(WITH_SUBSPACE);
|
||||||
let end = params.end.serialize(WITH_SUBSPACE);
|
let end = params.end.serialize(WITH_SUBSPACE);
|
||||||
|
|
||||||
let trx = self.read_trx().await?;
|
if !params.first {
|
||||||
let mut values = trx.get_ranges_keyvalues(
|
let mut has_more = true;
|
||||||
RangeOption {
|
let mut begin_selector = KeySelector::first_greater_or_equal(&begin);
|
||||||
begin: KeySelector::first_greater_or_equal(&begin),
|
|
||||||
end: KeySelector::first_greater_than(&end),
|
while has_more {
|
||||||
mode: if params.first {
|
let mut last_key_bytes = None;
|
||||||
options::StreamingMode::Small
|
|
||||||
|
{
|
||||||
|
let trx = self.timed_read_trx().await?;
|
||||||
|
let mut values = trx.as_ref().get_ranges(
|
||||||
|
RangeOption {
|
||||||
|
begin: begin_selector,
|
||||||
|
end: KeySelector::first_greater_than(&end),
|
||||||
|
mode: options::StreamingMode::WantAll,
|
||||||
|
reverse: !params.ascending,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
while let Some(values) = values.try_next().await? {
|
||||||
|
has_more = values.more();
|
||||||
|
let mut last_key = &[] as &[u8];
|
||||||
|
|
||||||
|
for value in values.iter() {
|
||||||
|
last_key = value.key();
|
||||||
|
if !cb(last_key.get(1..).unwrap_or_default(), value.value())? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if has_more && trx.is_expired() {
|
||||||
|
last_key_bytes = last_key.to_vec().into();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(last_key_bytes) = last_key_bytes {
|
||||||
|
begin = last_key_bytes;
|
||||||
|
begin_selector = KeySelector::first_greater_than(&begin);
|
||||||
} else {
|
} else {
|
||||||
options::StreamingMode::WantAll
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let trx = self.read_trx().await?;
|
||||||
|
let mut values = trx.get_ranges_keyvalues(
|
||||||
|
RangeOption {
|
||||||
|
begin: KeySelector::first_greater_or_equal(&begin),
|
||||||
|
end: KeySelector::first_greater_than(&end),
|
||||||
|
mode: options::StreamingMode::Small,
|
||||||
|
reverse: !params.ascending,
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
reverse: !params.ascending,
|
true,
|
||||||
..Default::default()
|
);
|
||||||
},
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
while let Some(value) = values.try_next().await? {
|
if let Some(value) = values.try_next().await? {
|
||||||
let key = value.key().get(1..).unwrap_or_default();
|
cb(value.key().get(1..).unwrap_or_default(), value.value())?;
|
||||||
let value = value.value();
|
|
||||||
|
|
||||||
if !cb(key, value)? || params.first {
|
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,6 +178,13 @@ impl FdbStore {
|
||||||
|
|
||||||
Ok(trx)
|
Ok(trx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn timed_read_trx(&self) -> crate::Result<TimedTransaction> {
|
||||||
|
self.db
|
||||||
|
.create_trx()
|
||||||
|
.map_err(Into::into)
|
||||||
|
.map(TimedTransaction::new)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn read_chunked_value(
|
pub(crate) async fn read_chunked_value(
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,7 @@ pub async fn test() {
|
||||||
pop3.send("DELE 2").await;
|
pop3.send("DELE 2").await;
|
||||||
pop3.assert_read(ResponseType::Ok).await;
|
pop3.assert_read(ResponseType::Ok).await;
|
||||||
pop3.send("QUIT").await;
|
pop3.send("QUIT").await;
|
||||||
|
pop3.assert_read(ResponseType::Ok).await;
|
||||||
let mut pop3 = Pop3Connection::connect_and_login().await;
|
let mut pop3 = Pop3Connection::connect_and_login().await;
|
||||||
pop3.send("STAT").await;
|
pop3.send("STAT").await;
|
||||||
pop3.assert_read(ResponseType::Ok)
|
pop3.assert_read(ResponseType::Ok)
|
||||||
|
|
@ -179,6 +180,7 @@ pub async fn test() {
|
||||||
pop3.assert_read(ResponseType::Ok).await;
|
pop3.assert_read(ResponseType::Ok).await;
|
||||||
pop3.assert_read(ResponseType::Ok).await;
|
pop3.assert_read(ResponseType::Ok).await;
|
||||||
pop3.send("QUIT").await;
|
pop3.send("QUIT").await;
|
||||||
|
pop3.assert_read(ResponseType::Ok).await;
|
||||||
let mut pop3 = Pop3Connection::connect_and_login().await;
|
let mut pop3 = Pop3Connection::connect_and_login().await;
|
||||||
pop3.send("STAT").await;
|
pop3.send("STAT").await;
|
||||||
pop3.assert_read(ResponseType::Ok)
|
pop3.assert_read(ResponseType::Ok)
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,100 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::{collections::HashSet, time::Duration};
|
||||||
|
|
||||||
use jmap_proto::types::{collection::Collection, property::Property};
|
use jmap_proto::types::{collection::Collection, property::Property};
|
||||||
use store::{
|
use store::{
|
||||||
write::{
|
write::{
|
||||||
BatchBuilder, BitmapClass, DirectoryClass, MaybeDynamicId, TagValue, ValueClass, F_CLEAR,
|
BatchBuilder, BitmapClass, DirectoryClass, MaybeDynamicId, TagValue, ValueClass, F_CLEAR,
|
||||||
},
|
},
|
||||||
BitmapKey, Store, ValueKey,
|
BitmapKey, IterateParams, Store, ValueKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
// FDB max value
|
// FDB max value
|
||||||
const MAX_VALUE_SIZE: usize = 100000;
|
const MAX_VALUE_SIZE: usize = 100000;
|
||||||
|
|
||||||
pub async fn test(db: Store) {
|
pub async fn test(db: Store) {
|
||||||
|
#[cfg(feature = "foundationdb")]
|
||||||
|
if matches!(db, Store::FoundationDb(_)) && std::env::var("SLOW_FDB_TRX").is_ok() {
|
||||||
|
println!("Running slow FoundationDB transaction tests...");
|
||||||
|
|
||||||
|
// Create 900000 keys
|
||||||
|
let mut batch = BatchBuilder::new();
|
||||||
|
batch
|
||||||
|
.with_account_id(0)
|
||||||
|
.with_collection(0)
|
||||||
|
.update_document(0);
|
||||||
|
for n in 0..900000 {
|
||||||
|
batch.set(
|
||||||
|
ValueClass::Config(format!("key{n:10}").into_bytes()),
|
||||||
|
format!("value{n:10}").into_bytes(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if n % 10000 == 0 {
|
||||||
|
db.write(batch.build_batch()).await.unwrap();
|
||||||
|
batch = BatchBuilder::new();
|
||||||
|
batch
|
||||||
|
.with_account_id(0)
|
||||||
|
.with_collection(0)
|
||||||
|
.update_document(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.write(batch.build_batch()).await.unwrap();
|
||||||
|
println!("Created 900.000 keys...");
|
||||||
|
|
||||||
|
// Iterate over all keys
|
||||||
|
let mut n = 0;
|
||||||
|
db.iterate(
|
||||||
|
IterateParams::new(
|
||||||
|
ValueKey {
|
||||||
|
account_id: 0,
|
||||||
|
collection: 0,
|
||||||
|
document_id: 0,
|
||||||
|
class: ValueClass::Config(b"".to_vec()),
|
||||||
|
},
|
||||||
|
ValueKey {
|
||||||
|
account_id: 0,
|
||||||
|
collection: 0,
|
||||||
|
document_id: 0,
|
||||||
|
class: ValueClass::Config(b"\xFF".to_vec()),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|key, value| {
|
||||||
|
assert_eq!(std::str::from_utf8(key).unwrap(), format!("key{n:10}"));
|
||||||
|
assert_eq!(std::str::from_utf8(value).unwrap(), format!("value{n:10}"));
|
||||||
|
n += 1;
|
||||||
|
if n % 10000 == 0 {
|
||||||
|
println!("Iterated over {n} keys");
|
||||||
|
std::thread::sleep(Duration::from_millis(1000));
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Delete 100 keys
|
||||||
|
let mut batch = BatchBuilder::new();
|
||||||
|
batch
|
||||||
|
.with_account_id(0)
|
||||||
|
.with_collection(0)
|
||||||
|
.update_document(0);
|
||||||
|
for n in 0..900000 {
|
||||||
|
batch.clear(ValueClass::Config(format!("key{n:10}").into_bytes()));
|
||||||
|
|
||||||
|
if n % 10000 == 0 {
|
||||||
|
db.write(batch.build_batch()).await.unwrap();
|
||||||
|
batch = BatchBuilder::new();
|
||||||
|
batch
|
||||||
|
.with_account_id(0)
|
||||||
|
.with_collection(0)
|
||||||
|
.update_document(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.write(batch.build_batch()).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
// Testing ID assignment
|
// Testing ID assignment
|
||||||
println!("Running dynamic ID assignment tests...");
|
println!("Running dynamic ID assignment tests...");
|
||||||
let mut builder = BatchBuilder::new();
|
let mut builder = BatchBuilder::new();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue