Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
[package]
|
||||
name = "store"
|
||||
version = "0.16.22"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
utils = { path = "../utils" }
|
||||
types = { path = "../types" }
|
||||
nlp = { path = "../nlp" }
|
||||
trc = { path = "../trc" }
|
||||
registry = { path = "../registry" }
|
||||
rocksdb = { version = "0.24", optional = true, features = ["multi-threaded-cf"] }
|
||||
foundationdb = { version = "0.11", features = ["embedded-fdb-include", "fdb-7_4"], optional = true }
|
||||
rusqlite = { version = "0.40", features = ["bundled"], optional = true }
|
||||
rust-s3 = { version = "0.37", default-features = false, features = ["tokio-rustls-tls"], optional = true }
|
||||
reqwest_s3 = { package = "reqwest", version = "0.12", default-features = false, features = ["rustls-tls-native-roots"], optional = true }
|
||||
azure_core = { version = "0.21.0", optional = true }
|
||||
azure_storage = { version = "0.21.0", default-features = false, features = ["enable_reqwest_rustls", "hmac_rust"], optional = true }
|
||||
azure_storage_blobs = { version = "0.21.0", default-features = false, features = ["enable_reqwest_rustls", "hmac_rust"], optional = true }
|
||||
reqwest = { version = "0.13", default-features = false, features = ["rustls", "http2", "stream"]}
|
||||
tokio = { version = "1.53", features = ["sync", "fs", "io-util"] }
|
||||
r2d2 = { version = "0.8.10", optional = true }
|
||||
futures = { version = "0.3", optional = true }
|
||||
rand = "0.10.2"
|
||||
roaring = "0.11"
|
||||
rayon = { version = "1.12", optional = true }
|
||||
serde = { version = "1.0", features = ["derive"]}
|
||||
ahash = { version = "0.8.12", features = ["serde"] }
|
||||
xxhash-rust = { version = "0.8.18", features = ["xxh3"] }
|
||||
parking_lot = "0.12"
|
||||
lru-cache = { version = "0.1.2", optional = true }
|
||||
num_cpus = { version = "1.17", optional = true }
|
||||
blake3 = "1.8"
|
||||
lz4_flex = { version = "0.14", features = ["alloc"], default-features = false }
|
||||
deadpool-postgres = { version = "0.14", optional = true }
|
||||
tokio-postgres = { version = "0.7.18", features = ["with-serde_json-1"], optional = true }
|
||||
tokio-rustls = { version = "0.26", optional = true, default-features = false, features = ["aws_lc_rs", "tls12"] }
|
||||
rustls = { version = "0.23.43", optional = true, default-features = false, features = ["std", "aws_lc_rs", "tls12"] }
|
||||
rustls-pki-types = { version = "1", optional = true }
|
||||
aws-lc-rs = { version = "1", optional = true }
|
||||
x509-parser = { version = "0.18", optional = true }
|
||||
bytes = { version = "1.12", optional = true }
|
||||
mysql_async = { version = "0.37", default-features = false, features = ["default-rustls", "minimal"], optional = true }
|
||||
serde_json = { version = "1.0.151" }
|
||||
flate2 = "1.1"
|
||||
redis = { version = "1.6", features = [ "tokio-comp", "tokio-rustls-comp", "tls-rustls-insecure", "tls-rustls", "cluster-async", "sentinel"], optional = true }
|
||||
deadpool = { version = "0.13", features = ["managed"], optional = true }
|
||||
arc-swap = "1.9.2"
|
||||
bitpacking = "0.9.3"
|
||||
rkyv = { version = "0.8.18", features = ["little_endian"] }
|
||||
compact_str = "0.10.0"
|
||||
gethostname = "1.1.0"
|
||||
radsort = "0.1.1"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.53", features = ["full"] }
|
||||
|
||||
[features]
|
||||
# Data Stores
|
||||
rocks = ["rocksdb", "rayon", "num_cpus"]
|
||||
sqlite = ["rusqlite", "rayon", "r2d2", "num_cpus", "lru-cache"]
|
||||
postgres = ["tokio-postgres", "deadpool", "deadpool-postgres", "tokio-rustls", "rustls", "aws-lc-rs", "rustls-pki-types", "x509-parser", "futures", "bytes"]
|
||||
mysql = ["mysql_async", "futures"]
|
||||
foundation = ["foundationdb", "futures"]
|
||||
fdb-chunked-bm = []
|
||||
|
||||
# Blob stores
|
||||
s3 = ["rust-s3", "dep:reqwest_s3"]
|
||||
azure = ["azure_core", "azure_storage", "azure_storage_blobs", "futures"]
|
||||
|
||||
# In-memory stores
|
||||
redis = ["dep:redis", "deadpool", "deadpool/rt_tokio_1", "futures"]
|
||||
|
||||
enterprise = []
|
||||
test_mode = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use azure_core::error::ErrorKind;
|
||||
use azure_core::{ExponentialRetryOptions, RetryOptions, StatusCode};
|
||||
use azure_storage::StorageCredentials;
|
||||
use azure_storage_blobs::prelude::{ClientBuilder, ContainerClient};
|
||||
use futures::stream::StreamExt;
|
||||
use registry::schema::structs::{self};
|
||||
use std::sync::Arc;
|
||||
use std::{fmt::Display, io::Write, ops::Range};
|
||||
use utils::codec::base32_custom::Base32Writer;
|
||||
|
||||
use crate::BlobStore;
|
||||
|
||||
pub struct AzureStore {
|
||||
client: ContainerClient,
|
||||
prefix: Option<String>,
|
||||
}
|
||||
|
||||
impl AzureStore {
|
||||
pub async fn open(config: structs::AzureStore) -> Result<BlobStore, String> {
|
||||
let credentials = match (
|
||||
config.access_key.secret().await?.map(|v| v.into_owned()),
|
||||
config.sas_token.secret().await?.map(|v| v.into_owned()),
|
||||
) {
|
||||
(Some(access_key), None) => {
|
||||
StorageCredentials::access_key(config.storage_account.clone(), access_key)
|
||||
}
|
||||
(None, Some(sas_token)) => match StorageCredentials::sas_token(sas_token) {
|
||||
Ok(cred) => cred,
|
||||
Err(err) => {
|
||||
return Err(format!("Failed to create credentials: {err:?}"));
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return Err(concat!(
|
||||
"Failed to create credentials: exactly one of ",
|
||||
"'azure-access-key' and 'sas-token' must be specified"
|
||||
)
|
||||
.to_string());
|
||||
}
|
||||
};
|
||||
|
||||
Ok(BlobStore::Azure(Arc::new(AzureStore {
|
||||
client: ClientBuilder::new(config.storage_account, credentials)
|
||||
.retry(RetryOptions::exponential(
|
||||
ExponentialRetryOptions::default().max_retries(config.max_retries as u32 * 2),
|
||||
))
|
||||
.container_client(config.container),
|
||||
prefix: config.key_prefix,
|
||||
})))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_blob(
|
||||
&self,
|
||||
key: &[u8],
|
||||
range: Range<usize>,
|
||||
) -> trc::Result<Option<Vec<u8>>> {
|
||||
let blob_client = self.client.blob_client(self.build_key(key));
|
||||
|
||||
let mut stream = blob_client.get();
|
||||
let mut buf = if range.end == usize::MAX {
|
||||
// Let's turn this into a proper RangeFrom.
|
||||
stream = stream.range(range.start..);
|
||||
// We don't know how big to expect the result to be.
|
||||
Vec::new()
|
||||
} else {
|
||||
stream = stream.range(range.clone());
|
||||
Vec::with_capacity(range.end - range.start)
|
||||
};
|
||||
let mut stream = stream.into_stream();
|
||||
|
||||
while let Some(response) = stream.next().await {
|
||||
let err = match response {
|
||||
Ok(chunks) => {
|
||||
let mut chunks = chunks.data;
|
||||
let mut err = None;
|
||||
while let Some(chunk) = chunks.next().await {
|
||||
match chunk {
|
||||
Ok(ref data) => {
|
||||
buf.extend(data);
|
||||
}
|
||||
Err(e) => {
|
||||
err = Some(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
err
|
||||
}
|
||||
Err(e) => Some(e),
|
||||
};
|
||||
|
||||
if let Some(e) = err {
|
||||
return if matches!(
|
||||
e.kind(),
|
||||
ErrorKind::HttpResponse {
|
||||
status: StatusCode::NotFound,
|
||||
..
|
||||
}
|
||||
) {
|
||||
Ok(None)
|
||||
} else {
|
||||
Err(trc::StoreEvent::AzureError.reason(e))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(buf))
|
||||
}
|
||||
|
||||
pub(crate) async fn put_blob(&self, key: &[u8], data: &[u8]) -> trc::Result<()> {
|
||||
let blob_client = self.client.blob_client(self.build_key(key));
|
||||
|
||||
// We unfortunately have to make a copy of `data`. This is because the Azure SDK wants to
|
||||
// coerce the body into a value of type azure_core::Body, which doesn't have a lifetime
|
||||
// parameter and so cannot hold any non-static references (directly or indirectly).
|
||||
let data = data.to_vec();
|
||||
|
||||
blob_client
|
||||
.put_block_blob(data)
|
||||
.into_future()
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_blob(&self, key: &[u8]) -> trc::Result<bool> {
|
||||
let blob_client = self.client.blob_client(self.build_key(key));
|
||||
|
||||
if let Err(e) = blob_client.delete().into_future().await {
|
||||
if matches!(
|
||||
e.kind(),
|
||||
ErrorKind::HttpResponse {
|
||||
status: StatusCode::NotFound,
|
||||
..
|
||||
}
|
||||
) {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(trc::StoreEvent::AzureError.reason(e))
|
||||
}
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
fn build_key(&self, key: &[u8]) -> String {
|
||||
if let Some(prefix) = &self.prefix {
|
||||
let mut writer =
|
||||
Base32Writer::with_raw_capacity(prefix.len() + (key.len().div_ceil(4) * 5));
|
||||
writer.push_string(prefix);
|
||||
writer.write_all(key).unwrap();
|
||||
writer.finalize()
|
||||
} else {
|
||||
Base32Writer::from_bytes(key).finalize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn into_error(err: impl Display) -> trc::Error {
|
||||
trc::StoreEvent::AzureError.reason(err)
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
SearchStore,
|
||||
backend::elastic::ElasticSearchStore,
|
||||
search::{
|
||||
CalendarSearchField, ContactSearchField, EmailSearchField, SearchableField,
|
||||
TracingSearchField,
|
||||
},
|
||||
};
|
||||
use registry::schema::structs;
|
||||
use reqwest::{Error, Response, Url};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
impl ElasticSearchStore {
|
||||
pub async fn open(config: structs::ElasticSearchStore) -> Result<SearchStore, String> {
|
||||
Url::parse(&config.url).map_err(|e| format!("Invalid URL: {e}",))?;
|
||||
|
||||
Ok(SearchStore::ElasticSearch(Arc::new(Self {
|
||||
client: config
|
||||
.http_auth
|
||||
.build_http_client(
|
||||
config.http_headers,
|
||||
"application/json".into(),
|
||||
config.timeout,
|
||||
config.allow_invalid_certs,
|
||||
)
|
||||
.await?,
|
||||
url: config.url,
|
||||
num_replicas: config.num_replicas as usize,
|
||||
num_shards: config.num_shards as usize,
|
||||
include_source: config.include_source,
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn create_indexes(&self) -> trc::Result<()> {
|
||||
self.create_index::<EmailSearchField>().await?;
|
||||
self.create_index::<CalendarSearchField>().await?;
|
||||
self.create_index::<ContactSearchField>().await?;
|
||||
self.create_index::<TracingSearchField>().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_index<T: SearchableField>(&self) -> trc::Result<()> {
|
||||
let mut mappings = serde_json::Map::new();
|
||||
mappings.insert(
|
||||
"properties".to_string(),
|
||||
Value::Object(
|
||||
T::primary_keys()
|
||||
.iter()
|
||||
.chain(T::all_fields())
|
||||
.map(|field| (field.field_name().to_string(), field.es_schema()))
|
||||
.collect::<serde_json::Map<String, Value>>(),
|
||||
),
|
||||
);
|
||||
if !self.include_source {
|
||||
mappings.insert("_source".to_string(), json!({ "enabled": false }));
|
||||
}
|
||||
let body = json!({
|
||||
"mappings": mappings,
|
||||
"settings": {
|
||||
"index.number_of_shards": self.num_shards,
|
||||
"index.number_of_replicas": self.num_replicas,
|
||||
"analysis": {
|
||||
"analyzer": {
|
||||
"default": {
|
||||
"type": "custom",
|
||||
"tokenizer": "standard",
|
||||
"filter": ["lowercase", "stemmer"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.put(format!("{}/{}", self.url, T::index().index_name()))
|
||||
.body(body.to_string())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
trc::StoreEvent::ElasticsearchError
|
||||
.reason(err)
|
||||
.details("Failed to create index")
|
||||
})?;
|
||||
|
||||
match response.status().as_u16() {
|
||||
200..300 => Ok(()),
|
||||
status @ (400..500) => {
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
if text.contains("resource_already_exists_exception") {
|
||||
// Index already exists, ignore
|
||||
Ok(())
|
||||
} else {
|
||||
Err(trc::StoreEvent::ElasticsearchError
|
||||
.reason(text)
|
||||
.ctx(trc::Key::Code, status))
|
||||
}
|
||||
}
|
||||
status => {
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
Err(trc::StoreEvent::ElasticsearchError
|
||||
.reason(text)
|
||||
.ctx(trc::Key::Code, status))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test_mode")]
|
||||
pub async fn drop_indexes(&self) -> trc::Result<()> {
|
||||
use crate::write::SearchIndex;
|
||||
|
||||
for index in &[
|
||||
SearchIndex::Email,
|
||||
SearchIndex::Calendar,
|
||||
SearchIndex::Contacts,
|
||||
SearchIndex::Tracing,
|
||||
] {
|
||||
assert_success(
|
||||
self.client
|
||||
.delete(format!("{}/{}", self.url, index.index_name()))
|
||||
.send()
|
||||
.await,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn assert_success(response: Result<Response, Error>) -> trc::Result<Response> {
|
||||
match response {
|
||||
Ok(response) => {
|
||||
let status = response.status();
|
||||
if status.is_success() {
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(trc::StoreEvent::ElasticsearchError
|
||||
.reason(response.text().await.unwrap_or_default())
|
||||
.ctx(trc::Key::Code, status.as_u16()))
|
||||
}
|
||||
}
|
||||
Err(err) => Err(trc::StoreEvent::ElasticsearchError.reason(err)),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::search::*;
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
pub mod main;
|
||||
pub mod search;
|
||||
|
||||
pub struct ElasticSearchStore {
|
||||
client: Client,
|
||||
url: String,
|
||||
num_shards: usize,
|
||||
num_replicas: usize,
|
||||
include_source: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SearchResponse {
|
||||
pub hits: Hits,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Hits {
|
||||
pub total: Total,
|
||||
pub hits: Vec<Hit>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Total {
|
||||
pub value: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Hit {
|
||||
#[serde(rename = "_id", deserialize_with = "deserialize_string_to_u64")]
|
||||
pub id: u64,
|
||||
pub sort: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DeleteByQueryResponse {
|
||||
pub deleted: u64,
|
||||
}
|
||||
|
||||
impl SearchField {
|
||||
pub fn es_schema(&self) -> Value {
|
||||
match self {
|
||||
SearchField::AccountId
|
||||
| SearchField::DocumentId
|
||||
| SearchField::Email(EmailSearchField::Size) => json!({
|
||||
"type": "integer"
|
||||
}),
|
||||
SearchField::Id
|
||||
| SearchField::Email(EmailSearchField::SentAt | EmailSearchField::ReceivedAt)
|
||||
| SearchField::Calendar(CalendarSearchField::Start)
|
||||
| SearchField::Tracing(TracingSearchField::QueueId | TracingSearchField::EventType) => {
|
||||
json!({
|
||||
"type": "long"
|
||||
})
|
||||
}
|
||||
SearchField::Email(EmailSearchField::HasAttachment) => json!({
|
||||
"type": "boolean"
|
||||
}),
|
||||
SearchField::Calendar(CalendarSearchField::Uid)
|
||||
| SearchField::Contact(ContactSearchField::Uid) => json!({
|
||||
"type": "keyword",
|
||||
}),
|
||||
SearchField::Email(
|
||||
EmailSearchField::From | EmailSearchField::To | EmailSearchField::Subject,
|
||||
) => json!({
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}),
|
||||
SearchField::Email(EmailSearchField::Headers) => {
|
||||
json!({
|
||||
"type": "object",
|
||||
"enabled": true
|
||||
})
|
||||
}
|
||||
#[cfg(feature = "test_mode")]
|
||||
SearchField::Email(EmailSearchField::Bcc | EmailSearchField::Cc) => {
|
||||
json!({
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
SearchField::Email(EmailSearchField::Bcc | EmailSearchField::Cc) => {
|
||||
json!({
|
||||
"type": "text"
|
||||
})
|
||||
}
|
||||
SearchField::Email(EmailSearchField::Body | EmailSearchField::Attachment)
|
||||
| SearchField::Calendar(
|
||||
CalendarSearchField::Title
|
||||
| CalendarSearchField::Description
|
||||
| CalendarSearchField::Location
|
||||
| CalendarSearchField::Owner
|
||||
| CalendarSearchField::Attendee,
|
||||
)
|
||||
| SearchField::Contact(
|
||||
ContactSearchField::Member
|
||||
| ContactSearchField::Kind
|
||||
| ContactSearchField::Name
|
||||
| ContactSearchField::Nickname
|
||||
| ContactSearchField::Organization
|
||||
| ContactSearchField::Email
|
||||
| ContactSearchField::Phone
|
||||
| ContactSearchField::OnlineService
|
||||
| ContactSearchField::Address
|
||||
| ContactSearchField::Note,
|
||||
)
|
||||
| SearchField::File(FileSearchField::Name | FileSearchField::Content)
|
||||
| SearchField::Tracing(TracingSearchField::Keywords) => json!({
|
||||
"type": "text"
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_string_to_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
<&str>::deserialize(deserializer)?
|
||||
.parse::<u64>()
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
backend::elastic::{
|
||||
DeleteByQueryResponse, ElasticSearchStore, SearchResponse, main::assert_success,
|
||||
},
|
||||
search::{
|
||||
IndexDocument, SearchComparator, SearchDocumentId, SearchField, SearchFilter,
|
||||
SearchOperator, SearchQuery, SearchValue,
|
||||
},
|
||||
write::SearchIndex,
|
||||
};
|
||||
use serde_json::{Map, Value, json};
|
||||
use std::fmt::Write;
|
||||
|
||||
impl ElasticSearchStore {
|
||||
pub async fn index(&self, documents: Vec<IndexDocument>) -> trc::Result<()> {
|
||||
let mut request = String::with_capacity(512);
|
||||
|
||||
for document in documents {
|
||||
let id = if let (Some(SearchValue::Uint(account_id)), Some(SearchValue::Uint(doc_id))) = (
|
||||
document.fields.get(&SearchField::AccountId),
|
||||
document.fields.get(&SearchField::DocumentId),
|
||||
) {
|
||||
*account_id << 32 | *doc_id
|
||||
} else if let Some(SearchValue::Uint(id)) = document.fields.get(&SearchField::Id) {
|
||||
*id
|
||||
} else {
|
||||
debug_assert!(false, "Document is missing required ID fields");
|
||||
continue;
|
||||
};
|
||||
|
||||
let _ = writeln!(
|
||||
&mut request,
|
||||
"{{\"index\":{{\"_index\":\"{}\",\"_id\":{id}}}}}",
|
||||
document.index.index_name()
|
||||
);
|
||||
json_serialize(&mut request, &document);
|
||||
request.push('\n');
|
||||
}
|
||||
|
||||
assert_success(
|
||||
self.client
|
||||
.post(format!("{}/_bulk", self.url))
|
||||
.body(request)
|
||||
.send()
|
||||
.await,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub async fn query<R: SearchDocumentId>(
|
||||
&self,
|
||||
index: SearchIndex,
|
||||
filters: &[SearchFilter],
|
||||
sort: &[SearchComparator],
|
||||
) -> trc::Result<Vec<R>> {
|
||||
let mut search_after: Option<Value> = None;
|
||||
let mut results = Vec::new();
|
||||
let mut has_more = true;
|
||||
|
||||
while has_more {
|
||||
let query = Map::from_iter(
|
||||
[
|
||||
Some(("query".to_string(), build_query(filters))),
|
||||
Some(("size".to_string(), Value::from(10_000))),
|
||||
Some(("_source".to_string(), Value::from(false))),
|
||||
Some((
|
||||
"sort".to_string(),
|
||||
build_sort(sort, R::field().field_name()),
|
||||
)),
|
||||
search_after
|
||||
.take()
|
||||
.map(|sa| ("search_after".to_string(), sa)),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
);
|
||||
|
||||
let response = assert_success(
|
||||
self.client
|
||||
.post(format!("{}/{}/_search", self.url, index.index_name()))
|
||||
.body(serde_json::to_string(&query).unwrap_or_default())
|
||||
.send()
|
||||
.await,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let text = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|err| trc::StoreEvent::ElasticsearchError.reason(err))?;
|
||||
|
||||
let response = serde_json::from_str::<SearchResponse>(&text).map_err(|err| {
|
||||
trc::StoreEvent::ElasticsearchError
|
||||
.reason(err)
|
||||
.details(text)
|
||||
})?;
|
||||
|
||||
has_more = response.hits.hits.len() == 10_000
|
||||
&& response.hits.hits.last().unwrap().sort.is_some();
|
||||
|
||||
for hit in response.hits.hits {
|
||||
search_after = hit.sort;
|
||||
results.push(R::from_u64(hit.id));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
pub async fn unindex(&self, filter: SearchQuery) -> trc::Result<u64> {
|
||||
if filter.filters.is_empty() {
|
||||
return Err(trc::StoreEvent::ElasticsearchError
|
||||
.reason("Unindex operation requires at least one filter"));
|
||||
}
|
||||
|
||||
let query = json!({
|
||||
"query": build_query(&filter.filters),
|
||||
});
|
||||
|
||||
let response = assert_success(
|
||||
self.client
|
||||
.post(format!(
|
||||
"{}/{}/_delete_by_query",
|
||||
self.url,
|
||||
filter.index.index_name()
|
||||
))
|
||||
.body(serde_json::to_string(&query).unwrap_or_default())
|
||||
.send()
|
||||
.await,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let response_body = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|err| trc::StoreEvent::ElasticsearchError.reason(err))?;
|
||||
|
||||
serde_json::from_str::<DeleteByQueryResponse>(&response_body)
|
||||
.map(|delete_response| delete_response.deleted)
|
||||
.map_err(|err| trc::StoreEvent::ElasticsearchError.reason(err))
|
||||
}
|
||||
|
||||
pub async fn refresh_index(&self, index: SearchIndex) -> trc::Result<()> {
|
||||
let url = format!("{}/{}/_refresh", self.url, index.index_name());
|
||||
|
||||
assert_success(self.client.post(url).send().await)
|
||||
.await
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
fn build_query(filters: &[SearchFilter]) -> Value {
|
||||
if filters.is_empty() {
|
||||
return json!({ "match_all": {} });
|
||||
}
|
||||
|
||||
let mut stack = Vec::new();
|
||||
let mut conditions = Vec::new();
|
||||
let mut logical_op = &SearchFilter::And;
|
||||
|
||||
for filter in filters {
|
||||
match filter {
|
||||
SearchFilter::Operator { field, op, value } => {
|
||||
if field.is_text() && matches!(op, SearchOperator::Equal | SearchOperator::Contains)
|
||||
{
|
||||
let SearchValue::Text { value, .. } = value else {
|
||||
debug_assert!(false, "Invalid value type for text field");
|
||||
continue;
|
||||
};
|
||||
|
||||
if op != &SearchOperator::Equal {
|
||||
conditions.push(json!({
|
||||
"match": { field.field_name(): {
|
||||
"query": value,
|
||||
"operator": "and"
|
||||
} }
|
||||
}));
|
||||
} else {
|
||||
conditions.push(json!({
|
||||
"match_phrase": { field.field_name(): value }
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
let value = match value {
|
||||
SearchValue::Text { value, .. } => json!(value),
|
||||
SearchValue::Int(value) => json!(value),
|
||||
SearchValue::Uint(value) => json!(value),
|
||||
SearchValue::Boolean(value) => json!(value),
|
||||
SearchValue::KeyValues(kv) => {
|
||||
let (key, value) = kv.iter().next().unwrap();
|
||||
|
||||
let cond = if !value.is_empty() {
|
||||
if op == &SearchOperator::Equal {
|
||||
json!({
|
||||
"term": {
|
||||
format!("{}.{}.keyword", field.field_name(), key): value
|
||||
}
|
||||
})
|
||||
} else {
|
||||
json!({
|
||||
"match": {
|
||||
format!("{}.{}", field.field_name(), key): value
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
json!({
|
||||
"exists": { "field": format!("{}.{}", field.field_name(), key) }
|
||||
})
|
||||
};
|
||||
|
||||
conditions.push(cond);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let cond = match op {
|
||||
SearchOperator::Equal | SearchOperator::Contains => json!({
|
||||
"term": { field.field_name(): value }
|
||||
}),
|
||||
op => {
|
||||
let op = match op {
|
||||
SearchOperator::LowerThan => "lt",
|
||||
SearchOperator::LowerEqualThan => "lte",
|
||||
SearchOperator::GreaterThan => "gt",
|
||||
SearchOperator::GreaterEqualThan => "gte",
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
json!({
|
||||
"range": { field.field_name(): { op: value } }
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
conditions.push(cond);
|
||||
}
|
||||
}
|
||||
|
||||
SearchFilter::And | SearchFilter::Or | SearchFilter::Not => {
|
||||
stack.push((logical_op, conditions));
|
||||
logical_op = filter;
|
||||
conditions = Vec::new();
|
||||
}
|
||||
SearchFilter::End => {
|
||||
if let Some((prev_logical_op, mut prev_conditions)) = stack.pop() {
|
||||
if !conditions.is_empty() {
|
||||
match logical_op {
|
||||
SearchFilter::And => {
|
||||
prev_conditions.push(json!({ "bool": { "must": conditions } }));
|
||||
}
|
||||
SearchFilter::Or => {
|
||||
prev_conditions.push(json!({ "bool": { "should": conditions } }));
|
||||
}
|
||||
SearchFilter::Not => {
|
||||
prev_conditions.push(json!({ "bool": { "must_not": conditions } }));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
logical_op = prev_logical_op;
|
||||
conditions = prev_conditions;
|
||||
}
|
||||
}
|
||||
SearchFilter::DocumentSet(_) => {
|
||||
debug_assert!(
|
||||
false,
|
||||
"DocumentSet filters are not supported in this backend"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug_assert!(
|
||||
!conditions.is_empty(),
|
||||
"No conditions were built for the query"
|
||||
);
|
||||
|
||||
if conditions.len() == 1 {
|
||||
conditions.pop().unwrap()
|
||||
} else {
|
||||
json!({ "bool": { "must": conditions } })
|
||||
}
|
||||
}
|
||||
|
||||
fn build_sort(sort: &[SearchComparator], tie_breaker: &str) -> Value {
|
||||
Value::Array(
|
||||
sort.iter()
|
||||
.filter_map(|comp| match comp {
|
||||
SearchComparator::Field { field, ascending } => {
|
||||
let field = if field.is_text() {
|
||||
format!("{}.keyword", field.field_name())
|
||||
} else {
|
||||
field.field_name().to_string()
|
||||
};
|
||||
|
||||
Some(json!({
|
||||
field: if *ascending { "asc" } else { "desc" }
|
||||
}))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.chain([json!({
|
||||
tie_breaker: "asc"
|
||||
})])
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn json_serialize(request: &mut String, document: &IndexDocument) {
|
||||
request.push('{');
|
||||
for (idx, (k, v)) in document.fields.iter().enumerate() {
|
||||
if idx > 0 {
|
||||
request.push(',');
|
||||
}
|
||||
|
||||
let _ = write!(request, "{:?}:", k.field_name());
|
||||
match v {
|
||||
SearchValue::Text { value, .. } => {
|
||||
json_serialize_str(request, value);
|
||||
}
|
||||
SearchValue::KeyValues(map) => {
|
||||
request.push('{');
|
||||
for (i, (key, value)) in map.iter().enumerate() {
|
||||
if i > 0 {
|
||||
request.push(',');
|
||||
}
|
||||
json_serialize_str(request, key);
|
||||
request.push(':');
|
||||
json_serialize_str(request, value);
|
||||
}
|
||||
request.push('}');
|
||||
}
|
||||
SearchValue::Int(v) => {
|
||||
let _ = write!(request, "{}", v);
|
||||
}
|
||||
SearchValue::Uint(v) => {
|
||||
let _ = write!(request, "{}", v);
|
||||
}
|
||||
SearchValue::Boolean(v) => {
|
||||
let _ = write!(request, "{}", v);
|
||||
}
|
||||
}
|
||||
}
|
||||
request.push('}');
|
||||
}
|
||||
|
||||
fn json_serialize_str(request: &mut String, value: &str) {
|
||||
request.push('"');
|
||||
for c in value.chars() {
|
||||
match c {
|
||||
'"' => request.push_str("\\\""),
|
||||
'\\' => request.push_str("\\\\"),
|
||||
'\n' => request.push_str("\\n"),
|
||||
'\r' => request.push_str("\\r"),
|
||||
'\t' => request.push_str("\\t"),
|
||||
'\u{0008}' => request.push_str("\\b"), // backspace
|
||||
'\u{000C}' => request.push_str("\\f"), // form feed
|
||||
_ => {
|
||||
if !c.is_control() {
|
||||
request.push(c);
|
||||
} else {
|
||||
let _ = write!(request, "\\u{:04x}", c as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
request.push('"');
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::EphemeralStore;
|
||||
use crate::SUBSPACE_BLOBS;
|
||||
use std::ops::Range;
|
||||
|
||||
impl EphemeralStore {
|
||||
pub(crate) async fn get_blob(
|
||||
&self,
|
||||
key: &[u8],
|
||||
range: Range<usize>,
|
||||
) -> trc::Result<Option<Vec<u8>>> {
|
||||
let state = self.state.read();
|
||||
Ok(state
|
||||
.subspaces
|
||||
.get(&SUBSPACE_BLOBS)
|
||||
.and_then(|m| m.get(key))
|
||||
.map(|bytes| {
|
||||
if range.start == 0 && range.end == usize::MAX {
|
||||
bytes.clone()
|
||||
} else {
|
||||
bytes
|
||||
.get(range.start..std::cmp::min(bytes.len(), range.end))
|
||||
.unwrap_or_default()
|
||||
.to_vec()
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) async fn put_blob(&self, key: &[u8], data: &[u8]) -> trc::Result<()> {
|
||||
let mut state = self.state.write();
|
||||
state
|
||||
.subspaces
|
||||
.entry(SUBSPACE_BLOBS)
|
||||
.or_default()
|
||||
.insert(key.to_vec(), data.to_vec());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_blob(&self, key: &[u8]) -> trc::Result<bool> {
|
||||
let mut state = self.state.write();
|
||||
if let Some(map) = state.subspaces.get_mut(&SUBSPACE_BLOBS) {
|
||||
map.remove(key);
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{EphemeralState, EphemeralStore};
|
||||
use crate::Store;
|
||||
use ahash::AHashMap;
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
|
||||
impl EphemeralStore {
|
||||
pub fn open() -> Store {
|
||||
Store::Ephemeral(Arc::new(EphemeralStore {
|
||||
state: RwLock::new(EphemeralState {
|
||||
subspaces: AHashMap::new(),
|
||||
}),
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod blob;
|
||||
pub mod main;
|
||||
pub mod read;
|
||||
pub mod write;
|
||||
|
||||
use ahash::AHashMap;
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub struct EphemeralStore {
|
||||
pub(crate) state: RwLock<EphemeralState>,
|
||||
}
|
||||
|
||||
pub(crate) struct EphemeralState {
|
||||
pub(crate) subspaces: AHashMap<u8, BTreeMap<Vec<u8>, Vec<u8>>>,
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::EphemeralStore;
|
||||
use crate::{Deserialize, IterateParams, Key, ValueKey, write::ValueClass};
|
||||
|
||||
impl EphemeralStore {
|
||||
pub(crate) async fn get_value<U>(&self, key: impl Key) -> trc::Result<Option<U>>
|
||||
where
|
||||
U: Deserialize + 'static,
|
||||
{
|
||||
let subspace = key.subspace();
|
||||
let key_bytes = key.serialize(0);
|
||||
let state = self.state.read();
|
||||
match state
|
||||
.subspaces
|
||||
.get(&subspace)
|
||||
.and_then(|m| m.get(&key_bytes))
|
||||
{
|
||||
Some(value) => U::deserialize_with_key(&key_bytes, value).map(Some),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn key_exists(&self, key: impl Key) -> trc::Result<bool> {
|
||||
let subspace = key.subspace();
|
||||
let key_bytes = key.serialize(0);
|
||||
let state = self.state.read();
|
||||
Ok(state
|
||||
.subspaces
|
||||
.get(&subspace)
|
||||
.is_some_and(|m| m.contains_key(&key_bytes)))
|
||||
}
|
||||
|
||||
pub(crate) async fn iterate<T: Key>(
|
||||
&self,
|
||||
params: IterateParams<T>,
|
||||
mut cb: impl for<'x> FnMut(&'x [u8], &'x [u8]) -> trc::Result<bool> + Sync + Send,
|
||||
) -> trc::Result<()> {
|
||||
let subspace = params.begin.subspace();
|
||||
let begin = params.begin.serialize(0);
|
||||
let end = params.end.serialize(0);
|
||||
let state = self.state.read();
|
||||
let Some(map) = state.subspaces.get(&subspace) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if params.ascending {
|
||||
for (k, v) in map.range(begin..=end) {
|
||||
if !cb(k.as_slice(), v.as_slice())? || params.first {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (k, v) in map.range(begin..=end).rev() {
|
||||
if !cb(k.as_slice(), v.as_slice())? || params.first {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_counter(
|
||||
&self,
|
||||
key: impl Into<ValueKey<ValueClass>> + Sync + Send,
|
||||
) -> trc::Result<i64> {
|
||||
let key = key.into();
|
||||
let subspace = key.subspace();
|
||||
let key_bytes = key.serialize(0);
|
||||
let state = self.state.read();
|
||||
match state
|
||||
.subspaces
|
||||
.get(&subspace)
|
||||
.and_then(|m| m.get(&key_bytes))
|
||||
{
|
||||
Some(bytes) => Ok(i64::from_le_bytes(bytes[..].try_into().map_err(|_| {
|
||||
trc::Error::corrupted_key(&key_bytes, Some(bytes.as_slice()), trc::location!())
|
||||
})?)),
|
||||
None => Ok(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::EphemeralStore;
|
||||
use crate::{
|
||||
IndexKey, Key, LogKey, SUBSPACE_COUNTER, SUBSPACE_IN_MEMORY_COUNTER, SUBSPACE_INDEXES,
|
||||
SUBSPACE_LOGS, SUBSPACE_QUOTA,
|
||||
backend::deserialize_i64_le,
|
||||
write::{AssignedIds, Batch, MergeResult, Operation, ValueClass, ValueOp},
|
||||
};
|
||||
|
||||
impl EphemeralStore {
|
||||
pub(crate) async fn write(&self, batch: Batch<'_>) -> trc::Result<AssignedIds> {
|
||||
let mut account_id = u32::MAX;
|
||||
let mut collection = u8::MAX;
|
||||
let mut document_id = u32::MAX;
|
||||
let mut change_id = 0u64;
|
||||
let mut result = AssignedIds::default();
|
||||
let has_changes = !batch.changes.is_empty();
|
||||
|
||||
let mut state = self.state.write();
|
||||
|
||||
if has_changes {
|
||||
let map = state.subspaces.entry(SUBSPACE_COUNTER).or_default();
|
||||
for &account_id in batch.changes.keys() {
|
||||
let key = ValueClass::ChangeId.serialize(account_id, 0, 0, 0);
|
||||
let next = match map.get(&key) {
|
||||
Some(bytes) => deserialize_i64_le(&key, bytes)? + 1,
|
||||
None => 1,
|
||||
};
|
||||
map.insert(key, next.to_le_bytes().to_vec());
|
||||
result.push_change_id(account_id, next as u64);
|
||||
}
|
||||
}
|
||||
|
||||
for op in batch.ops.iter_mut() {
|
||||
match op {
|
||||
Operation::AccountId {
|
||||
account_id: account_id_,
|
||||
} => {
|
||||
account_id = *account_id_;
|
||||
if has_changes {
|
||||
change_id = result.set_current_change_id(account_id)?;
|
||||
}
|
||||
}
|
||||
Operation::Collection {
|
||||
collection: collection_,
|
||||
} => {
|
||||
collection = u8::from(*collection_);
|
||||
}
|
||||
Operation::DocumentId {
|
||||
document_id: document_id_,
|
||||
} => {
|
||||
document_id = *document_id_;
|
||||
}
|
||||
Operation::Value { class, op } => {
|
||||
let subspace = class.subspace(collection);
|
||||
let key = class.serialize(account_id, collection, document_id, 0);
|
||||
let map = state.subspaces.entry(subspace).or_default();
|
||||
|
||||
match op {
|
||||
ValueOp::Set(value) => {
|
||||
map.insert(key, std::mem::take(value));
|
||||
}
|
||||
ValueOp::SetFnc(set_op) => {
|
||||
let value = (set_op.fnc)(&set_op.params, &result)?;
|
||||
map.insert(key, value);
|
||||
}
|
||||
ValueOp::MergeFnc(merge_op) => {
|
||||
let merge_result = (merge_op.fnc)(
|
||||
&merge_op.params,
|
||||
&result,
|
||||
map.get(&key).map(|v| v.as_slice()),
|
||||
)?;
|
||||
|
||||
match merge_result {
|
||||
MergeResult::Update(value) => {
|
||||
map.insert(key, value);
|
||||
}
|
||||
MergeResult::Delete => {
|
||||
map.remove(&key);
|
||||
}
|
||||
MergeResult::Skip => (),
|
||||
}
|
||||
}
|
||||
ValueOp::AtomicAdd(by) => {
|
||||
let current = match map.get(&key) {
|
||||
Some(bytes) => deserialize_i64_le(&key, bytes)?,
|
||||
None => 0,
|
||||
};
|
||||
let next = current + *by;
|
||||
map.insert(key, next.to_le_bytes().to_vec());
|
||||
}
|
||||
ValueOp::AddAndGet(by) => {
|
||||
let current = match map.get(&key) {
|
||||
Some(bytes) => deserialize_i64_le(&key, bytes)?,
|
||||
None => 0,
|
||||
};
|
||||
let next = current + *by;
|
||||
map.insert(key, next.to_le_bytes().to_vec());
|
||||
result.push_counter_id(next);
|
||||
}
|
||||
ValueOp::Clear => {
|
||||
map.remove(&key);
|
||||
}
|
||||
}
|
||||
}
|
||||
Operation::Index { field, key, set } => {
|
||||
let index_key = IndexKey {
|
||||
account_id,
|
||||
collection,
|
||||
document_id,
|
||||
field: *field,
|
||||
key: key.as_slice(),
|
||||
}
|
||||
.serialize(0);
|
||||
let map = state.subspaces.entry(SUBSPACE_INDEXES).or_default();
|
||||
if *set {
|
||||
map.insert(index_key, Vec::new());
|
||||
} else {
|
||||
map.remove(&index_key);
|
||||
}
|
||||
}
|
||||
Operation::Log { collection, set } => {
|
||||
let log_key = LogKey {
|
||||
account_id,
|
||||
collection: u8::from(*collection),
|
||||
change_id,
|
||||
}
|
||||
.serialize(0);
|
||||
let map = state.subspaces.entry(SUBSPACE_LOGS).or_default();
|
||||
map.insert(log_key, std::mem::take(set));
|
||||
}
|
||||
Operation::AssertValue {
|
||||
class,
|
||||
assert_value,
|
||||
} => {
|
||||
let subspace = class.subspace(collection);
|
||||
let key = class.serialize(account_id, collection, document_id, 0);
|
||||
let matches = state
|
||||
.subspaces
|
||||
.get(&subspace)
|
||||
.and_then(|m| m.get(&key))
|
||||
.map(|v| assert_value.matches(v.as_slice()))
|
||||
.unwrap_or_else(|| assert_value.is_none());
|
||||
|
||||
if !matches {
|
||||
return Err(trc::StoreEvent::AssertValueFailed.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_range(&self, from: impl Key, to: impl Key) -> trc::Result<()> {
|
||||
let subspace = from.subspace();
|
||||
let from_key = from.serialize(0);
|
||||
let to_key = to.serialize(0);
|
||||
let mut state = self.state.write();
|
||||
if let Some(map) = state.subspaces.get_mut(&subspace) {
|
||||
let keys: Vec<Vec<u8>> = map
|
||||
.range(from_key..to_key)
|
||||
.map(|(k, _)| k.clone())
|
||||
.collect();
|
||||
for k in keys {
|
||||
map.remove(&k);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn purge_store(&self) -> trc::Result<()> {
|
||||
let mut state = self.state.write();
|
||||
for subspace in [SUBSPACE_QUOTA, SUBSPACE_COUNTER, SUBSPACE_IN_MEMORY_COUNTER] {
|
||||
if let Some(map) = state.subspaces.get_mut(&subspace) {
|
||||
let keys: Vec<Vec<u8>> = map
|
||||
.iter()
|
||||
.filter_map(|(k, v)| {
|
||||
if v.len() == std::mem::size_of::<i64>()
|
||||
&& i64::from_le_bytes(v[..].try_into().unwrap()) == 0
|
||||
{
|
||||
Some(k.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
for k in keys {
|
||||
map.remove(&k);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{FdbStore, MAX_VALUE_SIZE};
|
||||
use crate::{
|
||||
IterateParams, SUBSPACE_BLOBS,
|
||||
backend::foundationdb::into_error,
|
||||
write::{AnyKey, key::KeySerializer},
|
||||
};
|
||||
use std::ops::Range;
|
||||
use trc::AddContext;
|
||||
use types::blob_hash::BLOB_HASH_LEN;
|
||||
|
||||
impl FdbStore {
|
||||
pub(crate) async fn get_blob(
|
||||
&self,
|
||||
key: &[u8],
|
||||
range: Range<usize>,
|
||||
) -> trc::Result<Option<Vec<u8>>> {
|
||||
let block_start = range.start / MAX_VALUE_SIZE;
|
||||
let bytes_start = range.start % MAX_VALUE_SIZE;
|
||||
let block_end = (range.end / MAX_VALUE_SIZE) + 1;
|
||||
|
||||
let begin = KeySerializer::new(key.len() + 2)
|
||||
.write(key)
|
||||
.write(block_start as u16)
|
||||
.finalize();
|
||||
let end = KeySerializer::new(key.len() + 2)
|
||||
.write(key)
|
||||
.write(block_end as u16)
|
||||
.finalize();
|
||||
let key_len = begin.len();
|
||||
|
||||
let mut blob_data: Option<Vec<u8>> = None;
|
||||
let blob_range = range.end - range.start;
|
||||
|
||||
self.iterate(
|
||||
IterateParams::new(
|
||||
AnyKey {
|
||||
subspace: SUBSPACE_BLOBS,
|
||||
key: begin,
|
||||
},
|
||||
AnyKey {
|
||||
subspace: SUBSPACE_BLOBS,
|
||||
key: end,
|
||||
},
|
||||
),
|
||||
|key, value| {
|
||||
if key.len() == key_len {
|
||||
if let Some(blob_data) = &mut blob_data {
|
||||
blob_data.extend_from_slice(
|
||||
value
|
||||
.get(
|
||||
..std::cmp::min(
|
||||
blob_range.saturating_sub(blob_data.len()),
|
||||
value.len(),
|
||||
),
|
||||
)
|
||||
.unwrap_or(&[]),
|
||||
);
|
||||
if blob_data.len() == blob_range {
|
||||
return Ok(false);
|
||||
}
|
||||
} else {
|
||||
let blob_size = if blob_range <= (5 * (1 << 20)) {
|
||||
blob_range
|
||||
} else if value.len() == MAX_VALUE_SIZE {
|
||||
MAX_VALUE_SIZE * 2
|
||||
} else {
|
||||
value.len()
|
||||
};
|
||||
let mut blob_data_ = Vec::with_capacity(blob_size);
|
||||
blob_data_.extend_from_slice(
|
||||
value
|
||||
.get(
|
||||
bytes_start
|
||||
..std::cmp::min(bytes_start + blob_range, value.len()),
|
||||
)
|
||||
.unwrap_or(&[]),
|
||||
);
|
||||
let is_done = blob_data_.len() == blob_range;
|
||||
blob_data = blob_data_.into();
|
||||
if is_done {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(blob_data)
|
||||
}
|
||||
|
||||
pub(crate) async fn put_blob(&self, key: &[u8], data: &[u8]) -> trc::Result<()> {
|
||||
const N_CHUNKS: usize = (1 << 5) - 1;
|
||||
let last_chunk = std::cmp::max(
|
||||
(data.len() / MAX_VALUE_SIZE)
|
||||
+ if !data.len().is_multiple_of(MAX_VALUE_SIZE) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
},
|
||||
1,
|
||||
) - 1;
|
||||
let mut trx = self.db.create_trx().map_err(into_error)?;
|
||||
|
||||
for (chunk_pos, chunk_bytes) in data.chunks(MAX_VALUE_SIZE).enumerate() {
|
||||
trx.set(
|
||||
&KeySerializer::new(key.len() + 3)
|
||||
.write(SUBSPACE_BLOBS)
|
||||
.write(key)
|
||||
.write(chunk_pos as u16)
|
||||
.finalize(),
|
||||
chunk_bytes,
|
||||
);
|
||||
if chunk_pos == last_chunk || (chunk_pos > 0 && chunk_pos % N_CHUNKS == 0) {
|
||||
self.commit(trx, false).await?;
|
||||
if chunk_pos < last_chunk {
|
||||
trx = self.db.create_trx().map_err(into_error)?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_blob(&self, key: &[u8]) -> trc::Result<bool> {
|
||||
if key.len() < BLOB_HASH_LEN {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let trx = self.db.create_trx().map_err(into_error)?;
|
||||
trx.clear_range(
|
||||
&KeySerializer::new(key.len() + 3)
|
||||
.write(SUBSPACE_BLOBS)
|
||||
.write(key)
|
||||
.write(0u16)
|
||||
.finalize(),
|
||||
&KeySerializer::new(key.len() + 3)
|
||||
.write(SUBSPACE_BLOBS)
|
||||
.write(key)
|
||||
.write(u16::MAX)
|
||||
.finalize(),
|
||||
);
|
||||
|
||||
self.commit(trx, false).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::FdbStore;
|
||||
use crate::Store;
|
||||
use foundationdb::{Database, api, api::NetworkAutoStop, options::DatabaseOption};
|
||||
use parking_lot::Mutex;
|
||||
use registry::schema::structs;
|
||||
use std::sync::Arc;
|
||||
|
||||
static FDB_NETWORK: Mutex<Option<NetworkAutoStop>> = Mutex::new(None);
|
||||
|
||||
impl FdbStore {
|
||||
pub async fn open(config: structs::FoundationDbStore) -> Result<Store, String> {
|
||||
{
|
||||
let mut guard = FDB_NETWORK.lock();
|
||||
if guard.is_none() {
|
||||
let network = unsafe {
|
||||
api::FdbApiBuilder::default()
|
||||
.build()
|
||||
.map_err(|err| format!("Failed to boot FoundationDB: {err:?}"))?
|
||||
.boot()
|
||||
.map_err(|err| format!("Failed to boot FoundationDB: {err:?}"))?
|
||||
};
|
||||
*guard = Some(network);
|
||||
}
|
||||
}
|
||||
|
||||
let db = Database::new(config.cluster_file.as_deref())
|
||||
.map_err(|err| format!("Failed to create FoundationDB database: {err:?}"))?;
|
||||
|
||||
if let Some(value) = config.transaction_timeout {
|
||||
db.set_option(DatabaseOption::TransactionTimeout(
|
||||
value.into_inner().as_millis() as i32,
|
||||
))
|
||||
.map_err(|err| format!("Failed to set option: {err:?}"))?;
|
||||
}
|
||||
if let Some(value) = config.transaction_retry_limit {
|
||||
db.set_option(DatabaseOption::TransactionRetryLimit(value as i32))
|
||||
.map_err(|err| format!("Failed to set option: {err:?}"))?;
|
||||
}
|
||||
if let Some(value) = config.transaction_retry_delay {
|
||||
db.set_option(DatabaseOption::TransactionMaxRetryDelay(
|
||||
value.into_inner().as_millis() as i32,
|
||||
))
|
||||
.map_err(|err| format!("Failed to set option: {err:?}"))?;
|
||||
}
|
||||
if let Some(value) = config.machine_id {
|
||||
db.set_option(DatabaseOption::MachineId(value))
|
||||
.map_err(|err| format!("Failed to set option: {err:?}"))?;
|
||||
}
|
||||
if let Some(value) = config.datacenter_id {
|
||||
db.set_option(DatabaseOption::DatacenterId(value))
|
||||
.map_err(|err| format!("Failed to set option: {err:?}"))?;
|
||||
}
|
||||
|
||||
Ok(Store::FoundationDb(Arc::new(Self {
|
||||
db,
|
||||
version: Default::default(),
|
||||
})))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use foundationdb::{Database, FdbError};
|
||||
use std::{
|
||||
sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub mod blob;
|
||||
pub mod main;
|
||||
pub mod read;
|
||||
pub mod write;
|
||||
|
||||
const MAX_VALUE_SIZE: usize = 100000;
|
||||
|
||||
const REFRESH_READ_VERSION_AFTER: Duration = Duration::from_secs(1);
|
||||
const MAX_READ_VERSION_AGE: Duration = Duration::from_secs(4);
|
||||
|
||||
pub struct FdbStore {
|
||||
db: Database,
|
||||
version: ReadVersion,
|
||||
}
|
||||
|
||||
pub(crate) struct ReadVersion {
|
||||
base: Instant,
|
||||
version: AtomicI64,
|
||||
obtained: AtomicU64,
|
||||
refreshing: AtomicBool,
|
||||
}
|
||||
|
||||
impl ReadVersion {
|
||||
fn now(&self) -> u64 {
|
||||
self.base.elapsed().as_nanos() as u64
|
||||
}
|
||||
|
||||
fn current(&self) -> i64 {
|
||||
self.version.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
fn age(&self) -> u64 {
|
||||
self.now()
|
||||
.saturating_sub(self.obtained.load(Ordering::Acquire))
|
||||
}
|
||||
|
||||
fn store_max(&self, version: i64) {
|
||||
let mut current = self.version.load(Ordering::Relaxed);
|
||||
while version > current {
|
||||
match self.version.compare_exchange_weak(
|
||||
current,
|
||||
version,
|
||||
Ordering::Release,
|
||||
Ordering::Relaxed,
|
||||
) {
|
||||
Ok(_) => break,
|
||||
Err(actual) => current = actual,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn refreshed(&self, version: i64) {
|
||||
self.store_max(version);
|
||||
self.obtained.store(self.now(), Ordering::Release);
|
||||
}
|
||||
|
||||
fn raise_floor(&self, version: i64) {
|
||||
self.store_max(version);
|
||||
}
|
||||
|
||||
fn expire(&self) {
|
||||
self.obtained.store(0, Ordering::Release);
|
||||
}
|
||||
|
||||
fn try_begin_refresh(&self) -> Option<RefreshGuard<'_>> {
|
||||
if self
|
||||
.refreshing
|
||||
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed)
|
||||
.is_ok()
|
||||
{
|
||||
Some(RefreshGuard(&self.refreshing))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ReadVersion {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base: Instant::now(),
|
||||
version: AtomicI64::new(0),
|
||||
obtained: AtomicU64::new(0),
|
||||
refreshing: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RefreshGuard<'a>(&'a AtomicBool);
|
||||
|
||||
impl Drop for RefreshGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.0.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn into_error(error: FdbError) -> trc::Error {
|
||||
trc::StoreEvent::FoundationdbError
|
||||
.reason(error.message())
|
||||
.ctx(trc::Key::Code, error.code())
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{
|
||||
FdbStore, MAX_READ_VERSION_AGE, MAX_VALUE_SIZE, REFRESH_READ_VERSION_AFTER, into_error,
|
||||
};
|
||||
use crate::{
|
||||
Deserialize, IterateParams, Key, ValueKey, WITH_SUBSPACE,
|
||||
backend::deserialize_i64_le,
|
||||
write::{MAX_COMMIT_ATTEMPTS, MAX_COMMIT_TIME, ValueClass, key::KeySerializer},
|
||||
};
|
||||
use foundationdb::{
|
||||
FdbError, KeySelector, RangeOption, Transaction,
|
||||
future::FdbSlice,
|
||||
options::{self},
|
||||
};
|
||||
use futures::TryStreamExt;
|
||||
use std::time::Instant;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) enum ChunkedValue {
|
||||
Single(FdbSlice),
|
||||
Chunked { n_chunks: u8, bytes: Vec<u8> },
|
||||
None,
|
||||
}
|
||||
|
||||
struct ChunkedValueCollector {
|
||||
key: Vec<u8>,
|
||||
bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl FdbStore {
|
||||
pub(crate) async fn get_value<U>(&self, key: impl Key) -> trc::Result<Option<U>>
|
||||
where
|
||||
U: Deserialize,
|
||||
{
|
||||
let key = key.serialize(WITH_SUBSPACE);
|
||||
let mut retry_count = 0;
|
||||
let start = Instant::now();
|
||||
|
||||
loop {
|
||||
let trx = self.read_trx().await?;
|
||||
|
||||
match read_chunked_value(&key, &trx, true).await {
|
||||
Ok(ChunkedValue::Single(bytes)) => {
|
||||
return U::deserialize_with_key(key.get(1..).unwrap_or_default(), &bytes)
|
||||
.map(Some);
|
||||
}
|
||||
Ok(ChunkedValue::Chunked { bytes, .. }) => {
|
||||
return U::deserialize_owned_with_key(key.get(1..).unwrap_or_default(), bytes)
|
||||
.map(Some);
|
||||
}
|
||||
Ok(ChunkedValue::None) => return Ok(None),
|
||||
Err(err) => {
|
||||
self.on_read_error(trx, err, &mut retry_count, start)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn key_exists(&self, key: impl Key) -> trc::Result<bool> {
|
||||
let key = key.serialize(WITH_SUBSPACE);
|
||||
let mut retry_count = 0;
|
||||
let start = Instant::now();
|
||||
|
||||
loop {
|
||||
let trx = self.read_trx().await?;
|
||||
|
||||
match read_chunked_value(&key, &trx, true).await {
|
||||
Ok(ChunkedValue::Single(_) | ChunkedValue::Chunked { .. }) => return Ok(true),
|
||||
Ok(ChunkedValue::None) => return Ok(false),
|
||||
Err(err) => {
|
||||
self.on_read_error(trx, err, &mut retry_count, start)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn iterate<T: Key>(
|
||||
&self,
|
||||
params: IterateParams<T>,
|
||||
mut cb: impl for<'x> FnMut(&'x [u8], &'x [u8]) -> trc::Result<bool> + Sync + Send,
|
||||
) -> trc::Result<()> {
|
||||
let begin = params.begin.serialize(WITH_SUBSPACE);
|
||||
let end = params.end.serialize(WITH_SUBSPACE);
|
||||
let mut retry_count = 0;
|
||||
let start = Instant::now();
|
||||
|
||||
if !params.first {
|
||||
let mut last_key = vec![];
|
||||
let mut chunked_key: Option<ChunkedValueCollector> = None;
|
||||
|
||||
'outer: loop {
|
||||
let begin_selector = if last_key.is_empty() {
|
||||
KeySelector::first_greater_or_equal(&begin)
|
||||
} else {
|
||||
KeySelector::first_greater_than(&last_key)
|
||||
};
|
||||
|
||||
let trx = self.read_trx().await?;
|
||||
let mut values = trx.get_ranges(
|
||||
RangeOption {
|
||||
begin: begin_selector,
|
||||
end: KeySelector::first_greater_than(&end),
|
||||
mode: options::StreamingMode::WantAll,
|
||||
reverse: !params.ascending,
|
||||
..Default::default()
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
let mut last_key_ = vec![];
|
||||
loop {
|
||||
match values.try_next().await {
|
||||
Ok(Some(values)) => {
|
||||
let mut key = &[] as &[u8];
|
||||
for value in values.iter() {
|
||||
key = value.key();
|
||||
|
||||
// Check whether we are collecting a chunked value
|
||||
let cb_key = key.get(1..).unwrap_or_default();
|
||||
let cb_value = value.value();
|
||||
|
||||
if let Some(chunk) = &mut chunked_key {
|
||||
if chunk.key.len() + 1 == cb_key.len()
|
||||
&& cb_key[..chunk.key.len()] == chunk.key[..]
|
||||
{
|
||||
// This is a chunk of the current value
|
||||
if params.values {
|
||||
chunk.bytes.extend_from_slice(cb_value);
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
// Return collected chunked value
|
||||
if !cb(&chunk.key, &chunk.bytes)? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Reset collector
|
||||
chunked_key = None;
|
||||
}
|
||||
}
|
||||
|
||||
if cb_value.len() < MAX_VALUE_SIZE {
|
||||
if !cb(cb_key, cb_value)? {
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
// Start collecting chunked value
|
||||
chunked_key = Some(ChunkedValueCollector {
|
||||
key: cb_key.to_vec(),
|
||||
bytes: if params.values {
|
||||
cb_value.to_vec()
|
||||
} else {
|
||||
Vec::new()
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if values.more() {
|
||||
last_key_ = key.to_vec();
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
// Return any chunked value collected
|
||||
if let Some(chunked_key) = chunked_key.take() {
|
||||
cb(&chunked_key.key, &chunked_key.bytes)?;
|
||||
}
|
||||
|
||||
break 'outer;
|
||||
}
|
||||
Err(e) => {
|
||||
drop(values);
|
||||
if e.code() == 1007 && !last_key_.is_empty() {
|
||||
// Transaction is too old to perform reads or be committed
|
||||
last_key = last_key_;
|
||||
continue 'outer;
|
||||
} else if e.is_retryable()
|
||||
&& retry_count < MAX_COMMIT_ATTEMPTS
|
||||
&& start.elapsed() < MAX_COMMIT_TIME
|
||||
{
|
||||
// Transient error such as a cached read version ahead of lagging
|
||||
// storage servers (code 1009); resume from the last key read,
|
||||
// refresh the read version and back off before retrying.
|
||||
if !last_key_.is_empty() {
|
||||
last_key = last_key_;
|
||||
}
|
||||
self.version.expire();
|
||||
trx.on_error(e).await.map_err(into_error)?;
|
||||
retry_count += 1;
|
||||
continue 'outer;
|
||||
} else {
|
||||
return Err(into_error(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loop {
|
||||
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()
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
match values.try_next().await {
|
||||
Ok(Some(value)) => {
|
||||
cb(value.key().get(1..).unwrap_or_default(), value.value())?;
|
||||
break;
|
||||
}
|
||||
Ok(None) => break,
|
||||
Err(e) => {
|
||||
drop(values);
|
||||
self.on_read_error(trx, e, &mut retry_count, start).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_counter(
|
||||
&self,
|
||||
key: impl Into<ValueKey<ValueClass>> + Sync + Send,
|
||||
) -> trc::Result<i64> {
|
||||
let key = key.into().serialize(WITH_SUBSPACE);
|
||||
let mut retry_count = 0;
|
||||
let start = Instant::now();
|
||||
|
||||
loop {
|
||||
let trx = self.read_trx().await?;
|
||||
match trx.get(&key, true).await {
|
||||
Ok(Some(bytes)) => return deserialize_i64_le(&key, &bytes),
|
||||
Ok(None) => return Ok(0),
|
||||
Err(e) => {
|
||||
self.on_read_error(trx, e, &mut retry_count, start).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_read_error(
|
||||
&self,
|
||||
trx: Transaction,
|
||||
err: FdbError,
|
||||
retry_count: &mut u32,
|
||||
start: Instant,
|
||||
) -> trc::Result<()> {
|
||||
if err.is_retryable()
|
||||
&& *retry_count < MAX_COMMIT_ATTEMPTS
|
||||
&& start.elapsed() < MAX_COMMIT_TIME
|
||||
{
|
||||
// The cached read version may be ahead of lagging storage servers under heavy write
|
||||
// load (code 1009); expire it so the retry obtains a fresh read version, then let
|
||||
// FoundationDB back off before retrying.
|
||||
self.version.expire();
|
||||
trx.on_error(err).await.map_err(into_error)?;
|
||||
*retry_count += 1;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(into_error(err))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn read_trx(&self) -> trc::Result<Transaction> {
|
||||
let trx = self.db.create_trx().map_err(into_error)?;
|
||||
let version = self.version.current();
|
||||
let age = self.version.age();
|
||||
|
||||
if version != 0 && age < MAX_READ_VERSION_AGE.as_nanos() as u64 {
|
||||
if age >= REFRESH_READ_VERSION_AFTER.as_nanos() as u64
|
||||
&& let Some(_guard) = self.version.try_begin_refresh()
|
||||
{
|
||||
let read_version = trx.get_read_version().await.map_err(into_error)?;
|
||||
self.version.refreshed(read_version);
|
||||
} else {
|
||||
trx.set_read_version(version);
|
||||
}
|
||||
} else {
|
||||
let read_version = trx.get_read_version().await.map_err(into_error)?;
|
||||
self.version.refreshed(read_version);
|
||||
}
|
||||
|
||||
Ok(trx)
|
||||
}
|
||||
|
||||
pub(crate) fn invalidate_read_snapshot(&self) {
|
||||
self.version.expire();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn read_chunked_value(
|
||||
key: &[u8],
|
||||
trx: &Transaction,
|
||||
snapshot: bool,
|
||||
) -> Result<ChunkedValue, FdbError> {
|
||||
if let Some(bytes) = trx.get(key, snapshot).await? {
|
||||
if bytes.len() < MAX_VALUE_SIZE {
|
||||
Ok(ChunkedValue::Single(bytes))
|
||||
} else {
|
||||
let mut value = Vec::with_capacity(bytes.len() * 2);
|
||||
value.extend_from_slice(&bytes);
|
||||
let mut key = KeySerializer::new(key.len() + 1)
|
||||
.write(key)
|
||||
.write(0u8)
|
||||
.finalize();
|
||||
|
||||
while let Some(bytes) = trx.get(&key, snapshot).await? {
|
||||
value.extend_from_slice(&bytes);
|
||||
*key.last_mut().unwrap() += 1;
|
||||
}
|
||||
|
||||
Ok(ChunkedValue::Chunked {
|
||||
bytes: value,
|
||||
n_chunks: *key.last().unwrap(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Ok(ChunkedValue::None)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{
|
||||
FdbStore, MAX_VALUE_SIZE, into_error,
|
||||
read::{ChunkedValue, read_chunked_value},
|
||||
};
|
||||
use crate::{
|
||||
backend::deserialize_i64_le,
|
||||
write::{
|
||||
AssignedIds, Batch, IndexPropertyClass, MAX_COMMIT_ATTEMPTS, MAX_COMMIT_TIME, MergeResult,
|
||||
Operation, QueueClass, RegistryClass, SearchIndexType, TaskQueueClass, TelemetryClass,
|
||||
ValueClass, ValueOp, key::KeySerializer,
|
||||
},
|
||||
*,
|
||||
};
|
||||
use foundationdb::{
|
||||
FdbError, KeySelector, RangeOption, Transaction,
|
||||
options::{self, MutationType},
|
||||
};
|
||||
use futures::TryStreamExt;
|
||||
use rand::RngExt;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::Ordering,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use trc::AddContext;
|
||||
|
||||
impl FdbStore {
|
||||
pub(crate) async fn write(&self, batch: Batch<'_>) -> trc::Result<AssignedIds> {
|
||||
let start = Instant::now();
|
||||
let mut retry_count = 0;
|
||||
let has_changes = !batch.changes.is_empty();
|
||||
|
||||
loop {
|
||||
let mut account_id = u32::MAX;
|
||||
let mut collection = u8::MAX;
|
||||
let mut document_id = u32::MAX;
|
||||
let mut change_id = 0u64;
|
||||
let mut result = AssignedIds::default();
|
||||
|
||||
let trx = self.db.create_trx().map_err(into_error)?;
|
||||
|
||||
if has_changes {
|
||||
for &account_id in batch.changes.keys() {
|
||||
debug_assert!(account_id != u32::MAX);
|
||||
let key = ValueClass::ChangeId.serialize(account_id, 0, 0, WITH_SUBSPACE);
|
||||
let change_id =
|
||||
if let Some(bytes) = trx.get(&key, false).await.map_err(into_error)? {
|
||||
deserialize_i64_le(&key, &bytes)? + 1
|
||||
} else {
|
||||
1
|
||||
};
|
||||
trx.set(&key, &change_id.to_le_bytes()[..]);
|
||||
result.push_change_id(account_id, change_id as u64);
|
||||
}
|
||||
}
|
||||
|
||||
for op in batch.ops.iter_mut() {
|
||||
match op {
|
||||
Operation::AccountId {
|
||||
account_id: account_id_,
|
||||
} => {
|
||||
account_id = *account_id_;
|
||||
if has_changes {
|
||||
change_id = result.set_current_change_id(account_id)?;
|
||||
}
|
||||
}
|
||||
Operation::Collection {
|
||||
collection: collection_,
|
||||
} => {
|
||||
collection = u8::from(*collection_);
|
||||
}
|
||||
Operation::DocumentId {
|
||||
document_id: document_id_,
|
||||
} => {
|
||||
document_id = *document_id_;
|
||||
}
|
||||
Operation::Value { class, op } => {
|
||||
let mut key =
|
||||
class.serialize(account_id, collection, document_id, WITH_SUBSPACE);
|
||||
|
||||
match op {
|
||||
ValueOp::Set(value) => {
|
||||
if !chunk_value(&trx, &mut key, value, class, None).await {
|
||||
trx.cancel();
|
||||
return Err(trc::StoreEvent::FoundationdbError
|
||||
.ctx(trc::Key::Reason, "Value is too large"));
|
||||
}
|
||||
}
|
||||
ValueOp::SetFnc(set_op) => {
|
||||
let value = (set_op.fnc)(&set_op.params, &result)?;
|
||||
if !chunk_value(&trx, &mut key, &value, class, None).await {
|
||||
trx.cancel();
|
||||
return Err(trc::StoreEvent::FoundationdbError
|
||||
.ctx(trc::Key::Reason, "Value is too large"));
|
||||
}
|
||||
}
|
||||
ValueOp::MergeFnc(merge_op) => {
|
||||
let (merge_result, prev_num_chunks) =
|
||||
match read_chunked_value(&key, &trx, false)
|
||||
.await
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
ChunkedValue::Single(slice) => (
|
||||
(merge_op.fnc)(
|
||||
&merge_op.params,
|
||||
&result,
|
||||
Some(slice.as_ref()),
|
||||
)?,
|
||||
1,
|
||||
),
|
||||
ChunkedValue::Chunked { bytes, n_chunks } => (
|
||||
(merge_op.fnc)(
|
||||
&merge_op.params,
|
||||
&result,
|
||||
Some(bytes.as_ref()),
|
||||
)?,
|
||||
n_chunks as usize + 1,
|
||||
),
|
||||
ChunkedValue::None => {
|
||||
((merge_op.fnc)(&merge_op.params, &result, None)?, 0)
|
||||
}
|
||||
};
|
||||
|
||||
match merge_result {
|
||||
MergeResult::Update(value) => {
|
||||
if !chunk_value(
|
||||
&trx,
|
||||
&mut key,
|
||||
&value,
|
||||
class,
|
||||
Some(prev_num_chunks),
|
||||
)
|
||||
.await
|
||||
{
|
||||
trx.cancel();
|
||||
return Err(trc::StoreEvent::FoundationdbError
|
||||
.ctx(trc::Key::Reason, "Value is too large"));
|
||||
}
|
||||
}
|
||||
MergeResult::Delete => {
|
||||
if prev_num_chunks > 1 {
|
||||
clear_chunks(&trx, &key, None).await;
|
||||
} else {
|
||||
trx.clear(&key);
|
||||
}
|
||||
}
|
||||
MergeResult::Skip => (),
|
||||
}
|
||||
}
|
||||
ValueOp::AtomicAdd(by) => {
|
||||
trx.atomic_op(&key, &by.to_le_bytes()[..], MutationType::Add);
|
||||
}
|
||||
ValueOp::AddAndGet(by) => {
|
||||
let num = if let Some(bytes) =
|
||||
trx.get(&key, false).await.map_err(into_error)?
|
||||
{
|
||||
deserialize_i64_le(&key, &bytes)? + *by
|
||||
} else {
|
||||
*by
|
||||
};
|
||||
trx.set(&key, &num.to_le_bytes()[..]);
|
||||
result.push_counter_id(num);
|
||||
}
|
||||
ValueOp::Clear => {
|
||||
if is_chunked_value(key[0], class) {
|
||||
clear_chunks(&trx, &key, None).await;
|
||||
} else {
|
||||
trx.clear(&key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Operation::Index { field, key, set } => {
|
||||
let key = IndexKey {
|
||||
account_id,
|
||||
collection,
|
||||
document_id,
|
||||
field: *field,
|
||||
key: &*key,
|
||||
}
|
||||
.serialize(WITH_SUBSPACE);
|
||||
|
||||
if *set {
|
||||
trx.set(&key, &[]);
|
||||
} else {
|
||||
trx.clear(&key);
|
||||
}
|
||||
}
|
||||
Operation::Log { collection, set } => {
|
||||
let key = LogKey {
|
||||
account_id,
|
||||
collection: u8::from(*collection),
|
||||
change_id,
|
||||
}
|
||||
.serialize(WITH_SUBSPACE);
|
||||
|
||||
trx.set(&key, set);
|
||||
}
|
||||
Operation::AssertValue {
|
||||
class,
|
||||
assert_value,
|
||||
} => {
|
||||
let key =
|
||||
class.serialize(account_id, collection, document_id, WITH_SUBSPACE);
|
||||
|
||||
let matches = match read_chunked_value(&key, &trx, false).await {
|
||||
Ok(ChunkedValue::Single(bytes)) => assert_value.matches(bytes.as_ref()),
|
||||
Ok(ChunkedValue::Chunked { bytes, .. }) => {
|
||||
assert_value.matches(bytes.as_ref())
|
||||
}
|
||||
Ok(ChunkedValue::None) => assert_value.is_none(),
|
||||
Err(_) => false,
|
||||
};
|
||||
|
||||
if !matches {
|
||||
trx.cancel();
|
||||
return Err(trc::StoreEvent::AssertValueFailed.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self
|
||||
.commit(
|
||||
trx,
|
||||
retry_count < MAX_COMMIT_ATTEMPTS && start.elapsed() < MAX_COMMIT_TIME,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(result);
|
||||
} else {
|
||||
let backoff = rand::rng().random_range(50..=100);
|
||||
tokio::time::sleep(Duration::from_millis(backoff)).await;
|
||||
retry_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn commit(&self, trx: Transaction, will_retry: bool) -> trc::Result<bool> {
|
||||
match trx.commit().await {
|
||||
Ok(result) => {
|
||||
let commit_version = result.committed_version().map_err(into_error)?;
|
||||
self.version.raise_floor(commit_version);
|
||||
Ok(true)
|
||||
}
|
||||
Err(err) => {
|
||||
if will_retry {
|
||||
err.on_error().await.map_err(into_error)?;
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(into_error(FdbError::from(err)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn purge_store(&self) -> trc::Result<()> {
|
||||
// Obtain all zero counters
|
||||
let mut delete_keys = Vec::new();
|
||||
for subspace in [SUBSPACE_COUNTER, SUBSPACE_QUOTA, SUBSPACE_IN_MEMORY_COUNTER] {
|
||||
let trx = self.db.create_trx().map_err(into_error)?;
|
||||
let from_key = [subspace, 0u8];
|
||||
let to_key = [subspace, u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX];
|
||||
|
||||
let mut values = trx.get_ranges_keyvalues(
|
||||
RangeOption {
|
||||
begin: KeySelector::first_greater_or_equal(&from_key[..]),
|
||||
end: KeySelector::first_greater_or_equal(&to_key[..]),
|
||||
mode: options::StreamingMode::WantAll,
|
||||
reverse: false,
|
||||
..Default::default()
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
while let Some(value) = values.try_next().await.map_err(into_error)? {
|
||||
if value.value().iter().all(|byte| *byte == 0) {
|
||||
delete_keys.push(value.key().to_vec());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if delete_keys.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Delete keys
|
||||
let integer = 0i64.to_le_bytes();
|
||||
for chunk in delete_keys.chunks(1024) {
|
||||
let mut retry_count = 0;
|
||||
loop {
|
||||
let trx = self.db.create_trx().map_err(into_error)?;
|
||||
for key in chunk {
|
||||
trx.atomic_op(key, &integer, MutationType::CompareAndClear);
|
||||
}
|
||||
|
||||
if self.commit(trx, retry_count < MAX_COMMIT_ATTEMPTS).await? {
|
||||
break;
|
||||
} else {
|
||||
retry_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_range(&self, from: impl Key, to: impl Key) -> trc::Result<()> {
|
||||
let from = from.serialize(WITH_SUBSPACE);
|
||||
let to = to.serialize(WITH_SUBSPACE);
|
||||
|
||||
let trx = self.db.create_trx().map_err(into_error)?;
|
||||
trx.clear_range(&from, &to);
|
||||
self.commit(trx, false).await.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
fn is_chunked_subspace(subspace: u8) -> bool {
|
||||
matches!(
|
||||
subspace,
|
||||
crate::SUBSPACE_PROPERTY
|
||||
| crate::SUBSPACE_SEARCH_INDEX
|
||||
| crate::SUBSPACE_QUEUE_MESSAGE
|
||||
| crate::SUBSPACE_TASK_QUEUE
|
||||
| crate::SUBSPACE_DIRECTORY
|
||||
| crate::SUBSPACE_REGISTRY
|
||||
| crate::SUBSPACE_DELETED_ITEMS
|
||||
| crate::SUBSPACE_SPAM_SAMPLES
|
||||
| crate::SUBSPACE_REPORT_IN
|
||||
| crate::SUBSPACE_REPORT_OUT
|
||||
| crate::SUBSPACE_TELEMETRY_SPAN
|
||||
)
|
||||
}
|
||||
|
||||
fn is_chunked_value(subspace: u8, class: &ValueClass) -> bool {
|
||||
is_chunked_subspace(subspace)
|
||||
&& match class {
|
||||
ValueClass::Property(_)
|
||||
| ValueClass::IndexProperty(IndexPropertyClass::Hash { .. })
|
||||
| ValueClass::Registry(RegistryClass::Item { .. })
|
||||
| ValueClass::Queue(QueueClass::Message(_))
|
||||
| ValueClass::TaskQueue(TaskQueueClass::Task { .. })
|
||||
| ValueClass::Telemetry(TelemetryClass::Span(_)) => true,
|
||||
ValueClass::SearchIndex(index) => matches!(index.typ, SearchIndexType::Document),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn clear_chunks(trx: &Transaction, key: &[u8], from_chunk: Option<u8>) {
|
||||
let to = KeySerializer::new(key.len() + 1)
|
||||
.write(key)
|
||||
.write(u8::MAX)
|
||||
.finalize();
|
||||
let from = match from_chunk {
|
||||
Some(from_chunk) => Cow::Owned(
|
||||
KeySerializer::new(key.len() + 1)
|
||||
.write(key)
|
||||
.write(from_chunk)
|
||||
.finalize(),
|
||||
),
|
||||
None => Cow::Borrowed(key),
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let mut chunks = trx.get_ranges_keyvalues(
|
||||
RangeOption {
|
||||
begin: KeySelector::first_greater_or_equal(from.as_ref()),
|
||||
end: KeySelector::first_greater_or_equal(to.as_slice()),
|
||||
mode: options::StreamingMode::WantAll,
|
||||
..Default::default()
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
while let Ok(Some(chunk)) = chunks.try_next().await {
|
||||
let found = chunk.key();
|
||||
debug_assert!(
|
||||
found.len() == key.len() + 1 || found == key,
|
||||
"chunk range of {key:?} holds foreign key {found:?}, clearing it would destroy data"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
trx.clear_range(from.as_ref(), &to);
|
||||
}
|
||||
|
||||
async fn chunk_value(
|
||||
trx: &Transaction,
|
||||
key: &mut Vec<u8>,
|
||||
value: &[u8],
|
||||
class: &ValueClass,
|
||||
prev_num_chunks: Option<usize>,
|
||||
) -> bool {
|
||||
let num_chunks = if value.len() > MAX_VALUE_SIZE {
|
||||
value.len().div_ceil(MAX_VALUE_SIZE)
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
if num_chunks > u8::MAX as usize {
|
||||
return false;
|
||||
}
|
||||
|
||||
if is_chunked_value(key[0], class)
|
||||
&& prev_num_chunks.is_none_or(|prev_num_chunks| prev_num_chunks > num_chunks)
|
||||
{
|
||||
clear_chunks(trx, key, Some((num_chunks - 1) as u8)).await;
|
||||
}
|
||||
|
||||
if value.len() > MAX_VALUE_SIZE {
|
||||
for (pos, chunk) in value.chunks(MAX_VALUE_SIZE).enumerate() {
|
||||
match pos.cmp(&1) {
|
||||
Ordering::Less => {}
|
||||
Ordering::Equal => {
|
||||
key.push(0);
|
||||
}
|
||||
Ordering::Greater => {
|
||||
*key.last_mut().unwrap() += 1;
|
||||
}
|
||||
}
|
||||
trx.set(key, chunk);
|
||||
}
|
||||
} else {
|
||||
trx.set(key, value);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::BlobStore;
|
||||
use registry::schema::structs;
|
||||
use std::{io::SeekFrom, ops::Range, path::PathBuf, sync::Arc};
|
||||
use tokio::{
|
||||
fs::{self, File},
|
||||
io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt},
|
||||
};
|
||||
use utils::codec::base32_custom::Base32Writer;
|
||||
|
||||
pub struct FsStore {
|
||||
path: PathBuf,
|
||||
hash_levels: usize,
|
||||
}
|
||||
|
||||
impl FsStore {
|
||||
pub async fn open(config: structs::FileSystemStore) -> Result<BlobStore, String> {
|
||||
let path = PathBuf::from(&config.path);
|
||||
if !path.exists() {
|
||||
fs::create_dir_all(&path)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create directory: {e}"))?;
|
||||
}
|
||||
|
||||
Ok(BlobStore::Fs(Arc::new(FsStore {
|
||||
path,
|
||||
hash_levels: std::cmp::min(config.depth as usize, 5),
|
||||
})))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_blob(
|
||||
&self,
|
||||
key: &[u8],
|
||||
range: Range<usize>,
|
||||
) -> trc::Result<Option<Vec<u8>>> {
|
||||
let blob_path = self.build_path(key);
|
||||
let blob_size = match fs::metadata(&blob_path).await {
|
||||
Ok(m) => m.len() as usize,
|
||||
Err(_) => return Ok(None),
|
||||
};
|
||||
let mut blob = File::open(&blob_path).await.map_err(into_error)?;
|
||||
|
||||
Ok(Some(if range.start != 0 || range.end != usize::MAX {
|
||||
let from_offset = if range.start < blob_size {
|
||||
range.start
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let mut buf = vec![0; (std::cmp::min(range.end, blob_size) - from_offset) as usize];
|
||||
|
||||
if from_offset > 0 {
|
||||
blob.seek(SeekFrom::Start(from_offset as u64))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
}
|
||||
blob.read_exact(&mut buf).await.map_err(into_error)?;
|
||||
buf
|
||||
} else {
|
||||
let mut buf = Vec::with_capacity(blob_size as usize);
|
||||
blob.read_to_end(&mut buf).await.map_err(into_error)?;
|
||||
buf
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) async fn put_blob(&self, key: &[u8], data: &[u8]) -> trc::Result<()> {
|
||||
let blob_path = self.build_path(key);
|
||||
|
||||
if fs::metadata(&blob_path)
|
||||
.await
|
||||
.map_or(true, |m| m.len() as usize != data.len())
|
||||
{
|
||||
fs::create_dir_all(blob_path.parent().unwrap())
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
let mut blob_file = File::create(&blob_path).await.map_err(into_error)?;
|
||||
blob_file.write_all(data).await.map_err(into_error)?;
|
||||
blob_file.flush().await.map_err(into_error)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_blob(&self, key: &[u8]) -> trc::Result<bool> {
|
||||
let blob_path = self.build_path(key);
|
||||
if fs::metadata(&blob_path).await.is_ok() {
|
||||
fs::remove_file(&blob_path).await.map_err(into_error)?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn build_path(&self, key: &[u8]) -> PathBuf {
|
||||
let mut path = self.path.clone();
|
||||
|
||||
for byte in key.iter().take(self.hash_levels) {
|
||||
path.push(format!("{:x}", byte));
|
||||
}
|
||||
path.push(Base32Writer::from_bytes(key).finalize());
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
fn into_error(err: std::io::Error) -> trc::Error {
|
||||
trc::StoreEvent::FilesystemError.reason(err)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{HttpStore, HttpStoreConfig, HttpStoreFormat};
|
||||
use crate::{InMemoryStore, LookupStores, registry::bootstrap::Bootstrap};
|
||||
use ahash::AHashMap;
|
||||
use arc_swap::ArcSwap;
|
||||
use registry::schema::structs::{self, HttpLookupFormat};
|
||||
use std::{
|
||||
collections::hash_map::Entry,
|
||||
sync::atomic::{AtomicBool, AtomicU64},
|
||||
};
|
||||
|
||||
impl LookupStores {
|
||||
pub async fn parse_http(&mut self, bp: &mut Bootstrap) {
|
||||
// Parse remote lists
|
||||
for http in bp.list_infallible::<structs::HttpLookup>().await {
|
||||
let id = http.id;
|
||||
let http = http.object;
|
||||
if !http.enable {
|
||||
continue;
|
||||
}
|
||||
|
||||
let http_config = HttpStoreConfig {
|
||||
url: http.url,
|
||||
retry: http.retry.as_secs(),
|
||||
refresh: http.refresh.as_secs(),
|
||||
timeout: http.timeout.into_inner(),
|
||||
gzipped: http.is_gzipped,
|
||||
max_size: http.max_size as usize,
|
||||
max_entries: http.max_entries as usize,
|
||||
max_entry_size: http.max_entry_size as usize,
|
||||
format: match http.format {
|
||||
HttpLookupFormat::List => HttpStoreFormat::List,
|
||||
HttpLookupFormat::Csv(csv) => HttpStoreFormat::Csv {
|
||||
index_key: csv.index_key as u32,
|
||||
index_value: csv.index_value.map(|v| v as u32),
|
||||
separator: csv.separator.chars().next().unwrap_or(','),
|
||||
skip_first: csv.skip_first,
|
||||
},
|
||||
},
|
||||
id: http.namespace,
|
||||
};
|
||||
|
||||
match self.stores.entry(http_config.id.as_str().into()) {
|
||||
Entry::Vacant(entry) => {
|
||||
let store = HttpStore {
|
||||
entries: ArcSwap::from_pointee(AHashMap::new()),
|
||||
expires: AtomicU64::new(0),
|
||||
in_flight: AtomicBool::new(false),
|
||||
config: http_config,
|
||||
client: utils::http::unpooled_http_client(false),
|
||||
};
|
||||
|
||||
entry.insert(InMemoryStore::Http(store.into()));
|
||||
}
|
||||
Entry::Occupied(_) => {
|
||||
bp.build_error(
|
||||
id,
|
||||
format!(
|
||||
"An lookup store with the {} namespace already exists",
|
||||
http_config.id
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::{
|
||||
io::{BufRead, BufReader},
|
||||
sync::{Arc, atomic::Ordering},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use compact_str::ToCompactString;
|
||||
use rand::seq::IndexedRandom;
|
||||
use utils::HttpLimitResponse;
|
||||
|
||||
use crate::{Value, backend::http::HttpStoreFormat, write::now};
|
||||
|
||||
use super::HttpStore;
|
||||
|
||||
const BROWSER_USER_AGENTS: [&str; 5] = [
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/120.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0",
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
];
|
||||
|
||||
pub(crate) trait HttpStoreGet {
|
||||
fn get(&self, key: &str) -> Option<Value<'static>>;
|
||||
fn contains(&self, key: &str) -> bool;
|
||||
fn refresh(&self);
|
||||
}
|
||||
|
||||
impl HttpStoreGet for Arc<HttpStore> {
|
||||
fn get(&self, key: &str) -> Option<Value<'static>> {
|
||||
self.refresh();
|
||||
self.entries.load().get(key).cloned()
|
||||
}
|
||||
|
||||
fn contains(&self, key: &str) -> bool {
|
||||
#[cfg(feature = "test_mode")]
|
||||
{
|
||||
if self.config.url.contains("phishtank.com")
|
||||
|| self.config.url.contains("openphish.com")
|
||||
{
|
||||
return (self.config.url.contains("open") && key.contains("open"))
|
||||
|| (self.config.url.contains("tank") && key.contains("tank"));
|
||||
} else if self.config.url.contains("disposable.github.io") {
|
||||
return key.ends_with("guerrillamail.com") || key.ends_with("disposable.org");
|
||||
} else if self.config.url.contains("free_email_provider_domains.txt") {
|
||||
return key.ends_with("gmail.com")
|
||||
|| key.ends_with("googlemail.com")
|
||||
|| key.ends_with("yahoomail.com")
|
||||
|| key.ends_with("outlook.com")
|
||||
|| key.ends_with("freemail.org");
|
||||
}
|
||||
}
|
||||
|
||||
self.refresh();
|
||||
self.entries.load().contains_key(key)
|
||||
}
|
||||
|
||||
fn refresh(&self) {
|
||||
if self.expires.load(Ordering::Relaxed) <= now() {
|
||||
let in_flight = self.in_flight.swap(true, Ordering::Relaxed);
|
||||
if !in_flight {
|
||||
let this = self.clone();
|
||||
tokio::spawn(async move {
|
||||
let expires = match this.try_refresh().await {
|
||||
Ok(list) => {
|
||||
this.entries.store(list.into());
|
||||
this.config.refresh
|
||||
}
|
||||
Err(err) => {
|
||||
trc::error!(err);
|
||||
this.config.retry
|
||||
}
|
||||
};
|
||||
|
||||
this.expires.store(now() + expires, Ordering::Relaxed);
|
||||
this.in_flight.store(false, Ordering::Relaxed);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpStore {
|
||||
async fn try_refresh(&self) -> trc::Result<AHashMap<String, Value<'static>>> {
|
||||
let time = Instant::now();
|
||||
let agent = BROWSER_USER_AGENTS.choose(&mut rand::rng()).unwrap();
|
||||
let response = self
|
||||
.client
|
||||
.get(&self.config.url)
|
||||
.timeout(self.config.timeout)
|
||||
.header(reqwest::header::USER_AGENT, *agent)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
trc::StoreEvent::HttpStoreError
|
||||
.into_err()
|
||||
.reason(err)
|
||||
.ctx(trc::Key::Url, self.config.url.to_compact_string())
|
||||
.details("Failed to build request")
|
||||
})?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
trc::bail!(
|
||||
trc::StoreEvent::HttpStoreError
|
||||
.into_err()
|
||||
.ctx(trc::Key::Code, response.status().as_u16())
|
||||
.ctx(trc::Key::Url, self.config.url.to_compact_string())
|
||||
.ctx(trc::Key::Elapsed, time.elapsed())
|
||||
.details("Failed to fetch HTTP list")
|
||||
);
|
||||
}
|
||||
|
||||
let bytes = response
|
||||
.bytes_with_limit(self.config.max_size)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
trc::StoreEvent::HttpStoreError
|
||||
.into_err()
|
||||
.reason(err)
|
||||
.ctx(trc::Key::Url, self.config.url.to_compact_string())
|
||||
.ctx(trc::Key::Elapsed, time.elapsed())
|
||||
.details("Failed to fetch resource")
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
trc::StoreEvent::HttpStoreError
|
||||
.into_err()
|
||||
.ctx(trc::Key::Url, self.config.url.to_compact_string())
|
||||
.ctx(trc::Key::Elapsed, time.elapsed())
|
||||
.details("Resource is too large")
|
||||
})?;
|
||||
|
||||
let reader: Box<dyn std::io::Read + Sync + Send> = if self.config.gzipped {
|
||||
Box::new(flate2::read::GzDecoder::new(&bytes[..]))
|
||||
} else {
|
||||
Box::new(&bytes[..])
|
||||
};
|
||||
|
||||
let mut entries = AHashMap::new();
|
||||
for (pos, line) in BufReader::new(reader).lines().enumerate() {
|
||||
let line_ = line.map_err(|err| {
|
||||
trc::StoreEvent::HttpStoreError
|
||||
.into_err()
|
||||
.reason(err)
|
||||
.ctx(trc::Key::Url, self.config.url.to_compact_string())
|
||||
.ctx(trc::Key::Elapsed, time.elapsed())
|
||||
.details("Failed to read line")
|
||||
})?;
|
||||
|
||||
match &self.config.format {
|
||||
HttpStoreFormat::List => {
|
||||
let line = line_.trim();
|
||||
if !line.is_empty() {
|
||||
entries.insert(line.to_string(), Value::Integer(1));
|
||||
}
|
||||
}
|
||||
HttpStoreFormat::Csv {
|
||||
index_key,
|
||||
index_value,
|
||||
separator,
|
||||
skip_first,
|
||||
} if pos > 0 || !*skip_first => {
|
||||
let mut in_quote = false;
|
||||
let mut col_num = 0;
|
||||
let mut last_ch = ' ';
|
||||
|
||||
let mut entry_key: String = String::new();
|
||||
let mut entry_value: String = String::new();
|
||||
|
||||
for ch in line_.chars() {
|
||||
match ch {
|
||||
'"' if last_ch != '\\' => {
|
||||
in_quote = !in_quote;
|
||||
}
|
||||
'\\' if last_ch != '\\' => (),
|
||||
_ => {
|
||||
if ch == *separator && !in_quote {
|
||||
if col_num == *index_key && index_value.is_none() {
|
||||
break;
|
||||
} else {
|
||||
col_num += 1;
|
||||
}
|
||||
} else if col_num == *index_key {
|
||||
entry_key.push(ch);
|
||||
if entry_key.len() > self.config.max_entry_size {
|
||||
break;
|
||||
}
|
||||
} else if index_value.is_some_and(|v| col_num == v) {
|
||||
entry_value.push(ch);
|
||||
if entry_value.len() > self.config.max_entry_size {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
last_ch = ch;
|
||||
}
|
||||
|
||||
if !entry_key.is_empty() {
|
||||
let entry_value = if !entry_value.is_empty() {
|
||||
Value::Text(entry_value.into())
|
||||
} else {
|
||||
Value::Integer(1)
|
||||
};
|
||||
entries.insert(entry_key, entry_value);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
if entries.len() == self.config.max_entries {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
trc::event!(
|
||||
Store(trc::StoreEvent::HttpStoreFetch),
|
||||
Url = self.config.url.to_compact_string(),
|
||||
Total = entries.len(),
|
||||
Elapsed = time.elapsed(),
|
||||
);
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod config;
|
||||
pub mod lookup;
|
||||
|
||||
use std::{
|
||||
sync::atomic::{AtomicBool, AtomicU64},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use arc_swap::ArcSwap;
|
||||
|
||||
use crate::Value;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HttpStoreConfig {
|
||||
pub id: String,
|
||||
pub url: String,
|
||||
pub retry: u64,
|
||||
pub refresh: u64,
|
||||
pub timeout: Duration,
|
||||
pub gzipped: bool,
|
||||
pub max_size: usize,
|
||||
pub max_entries: usize,
|
||||
pub max_entry_size: usize,
|
||||
pub format: HttpStoreFormat,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum HttpStoreFormat {
|
||||
List,
|
||||
Csv {
|
||||
index_key: u32,
|
||||
index_value: Option<u32>,
|
||||
separator: char,
|
||||
skip_first: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HttpStore {
|
||||
pub entries: ArcSwap<AHashMap<String, Value<'static>>>,
|
||||
pub expires: AtomicU64,
|
||||
pub in_flight: AtomicBool,
|
||||
pub config: HttpStoreConfig,
|
||||
pub client: reqwest::Client,
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
SearchStore,
|
||||
backend::meili::{MeiliSearchStore, Task, TaskStatus, TaskUid},
|
||||
search::{
|
||||
CalendarSearchField, ContactSearchField, EmailSearchField, SearchField, SearchableField,
|
||||
TracingSearchField,
|
||||
},
|
||||
write::now,
|
||||
};
|
||||
use registry::schema::structs;
|
||||
use reqwest::{Error, Response, Url};
|
||||
use serde_json::{Value, json};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
const UNCONFIRMED_TASK_RECHECK_DELAY: u64 = 600;
|
||||
pub(crate) const MAX_TOTAL_HITS: u64 = 100_000;
|
||||
|
||||
impl MeiliSearchStore {
|
||||
pub async fn open(config: structs::MeilisearchStore) -> Result<SearchStore, String> {
|
||||
let client = config
|
||||
.http_auth
|
||||
.build_http_client(
|
||||
config.http_headers,
|
||||
"application/json".into(),
|
||||
config.timeout,
|
||||
config.allow_invalid_certs,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Url::parse(&config.url).map_err(|e| format!("Invalid URL: {e}",))?;
|
||||
|
||||
let ms = Self {
|
||||
client,
|
||||
url: config.url,
|
||||
task_poll_interval: Duration::from_millis(500),
|
||||
task_poll_retries: 120,
|
||||
task_fail_on_timeout: true,
|
||||
};
|
||||
|
||||
if let Err(err) = ms.create_indexes().await {
|
||||
return Err(format!("Failed to create indexes: {err}"));
|
||||
}
|
||||
|
||||
Ok(SearchStore::MeiliSearch(Arc::new(MeiliSearchStore {
|
||||
client: ms.client,
|
||||
url: ms.url,
|
||||
task_poll_interval: config.poll_interval.into_inner(),
|
||||
task_poll_retries: config.max_retries as usize,
|
||||
task_fail_on_timeout: config.fail_on_timeout,
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn create_indexes(&self) -> trc::Result<()> {
|
||||
self.create_index::<EmailSearchField>().await?;
|
||||
self.create_index::<CalendarSearchField>().await?;
|
||||
self.create_index::<ContactSearchField>().await?;
|
||||
self.create_index::<TracingSearchField>().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn index_exists(&self, index_uid: &str) -> trc::Result<bool> {
|
||||
let response = self
|
||||
.client
|
||||
.get(format!("{}/indexes/{}", self.url, index_uid))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| trc::StoreEvent::MeilisearchError.reason(err))?;
|
||||
|
||||
match response.status().as_u16() {
|
||||
200..=299 => Ok(true),
|
||||
404 => Ok(false),
|
||||
status => {
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
Err(trc::StoreEvent::MeilisearchError
|
||||
.reason(text)
|
||||
.ctx(trc::Key::Code, status))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_index<T: SearchableField>(&self) -> trc::Result<()> {
|
||||
let index_name = T::index().index_name();
|
||||
|
||||
if self.index_exists(index_name).await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let response = assert_success(
|
||||
self.client
|
||||
.post(format!("{}/indexes", self.url))
|
||||
.body(
|
||||
json!({
|
||||
"uid": index_name,
|
||||
"primaryKey": "id",
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.send()
|
||||
.await,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !self.wait_for_task(response).await? {
|
||||
// Index already exists
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut searchable = Vec::new();
|
||||
let mut filterable = Vec::new();
|
||||
let mut sortable = Vec::new();
|
||||
|
||||
for field in T::all_fields() {
|
||||
if field.is_indexed() {
|
||||
sortable.push(Value::String(field.field_name().to_string()));
|
||||
}
|
||||
if field.is_text() {
|
||||
searchable.push(Value::String(field.field_name().to_string()));
|
||||
} else {
|
||||
filterable.push(Value::String(field.field_name().to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
for key in T::primary_keys() {
|
||||
filterable.push(Value::String(key.field_name().to_string()));
|
||||
if matches!(key, SearchField::Id) {
|
||||
sortable.push(Value::String(key.field_name().to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test_mode")]
|
||||
filterable.push(Value::String("bcc".into()));
|
||||
|
||||
if !searchable.is_empty() {
|
||||
self.update_index_settings(
|
||||
index_name,
|
||||
"searchable-attributes",
|
||||
Value::Array(searchable),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !filterable.is_empty() {
|
||||
self.update_index_settings(
|
||||
index_name,
|
||||
"filterable-attributes",
|
||||
Value::Array(filterable),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !sortable.is_empty() {
|
||||
self.update_index_settings(index_name, "sortable-attributes", Value::Array(sortable))
|
||||
.await?;
|
||||
}
|
||||
|
||||
self.update_index_pagination(index_name).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_index_pagination(&self, index_uid: &str) -> trc::Result<bool> {
|
||||
let response = assert_success(
|
||||
self.client
|
||||
.patch(format!(
|
||||
"{}/indexes/{}/settings/pagination",
|
||||
self.url, index_uid
|
||||
))
|
||||
.body(json!({ "maxTotalHits": MAX_TOTAL_HITS }).to_string())
|
||||
.send()
|
||||
.await,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.wait_for_task(response).await
|
||||
}
|
||||
|
||||
async fn update_index_settings(
|
||||
&self,
|
||||
index_uid: &str,
|
||||
setting: &str,
|
||||
value: Value,
|
||||
) -> trc::Result<bool> {
|
||||
let response = assert_success(
|
||||
self.client
|
||||
.put(format!(
|
||||
"{}/indexes/{}/settings/{}",
|
||||
self.url, index_uid, setting
|
||||
))
|
||||
.body(value.to_string())
|
||||
.send()
|
||||
.await,
|
||||
)
|
||||
.await?;
|
||||
self.wait_for_task(response).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "test_mode")]
|
||||
pub async fn drop_indexes(&self) -> trc::Result<()> {
|
||||
use crate::write::SearchIndex;
|
||||
|
||||
for index in &[
|
||||
SearchIndex::Email,
|
||||
SearchIndex::Calendar,
|
||||
SearchIndex::Contacts,
|
||||
SearchIndex::Tracing,
|
||||
] {
|
||||
let response = self
|
||||
.client
|
||||
.delete(format!("{}/indexes/{}", self.url, index.index_name()))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| trc::StoreEvent::MeilisearchError.reason(err))?;
|
||||
|
||||
match response.status().as_u16() {
|
||||
200..=299 => {
|
||||
self.wait_for_task(response).await?;
|
||||
}
|
||||
400..=499 => {
|
||||
// Index does not exist
|
||||
return Ok(());
|
||||
}
|
||||
_ => {
|
||||
let status = response.status();
|
||||
let msg = response.text().await.unwrap_or_default();
|
||||
return Err(trc::StoreEvent::MeilisearchError
|
||||
.reason(msg)
|
||||
.ctx(trc::Key::Code, status.as_u16()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn wait_for_task(&self, response: Response) -> trc::Result<bool> {
|
||||
let response_body = response.text().await.map_err(|err| {
|
||||
trc::StoreEvent::MeilisearchError
|
||||
.reason(err)
|
||||
.details("Request failed")
|
||||
})?;
|
||||
let task_uid = serde_json::from_str::<TaskUid>(&response_body)
|
||||
.map_err(|err| trc::StoreEvent::MeilisearchError.reason(err))?
|
||||
.task_uid;
|
||||
|
||||
let mut loop_count = 0;
|
||||
let url = format!("{}/tasks/{}", self.url, task_uid);
|
||||
|
||||
while loop_count < self.task_poll_retries {
|
||||
let resp = assert_success(self.client.get(&url).send().await).await?;
|
||||
|
||||
let text = resp
|
||||
.text()
|
||||
.await
|
||||
.map_err(|err| trc::StoreEvent::MeilisearchError.reason(err))?;
|
||||
|
||||
let task = serde_json::from_str::<Task>(&text).map_err(|err| {
|
||||
trc::StoreEvent::MeilisearchError
|
||||
.reason(err)
|
||||
.details(text.clone())
|
||||
})?;
|
||||
|
||||
match task.status {
|
||||
TaskStatus::Succeeded => return Ok(true),
|
||||
TaskStatus::Failed => {
|
||||
let (code, message) = task
|
||||
.error
|
||||
.map(|e| (e.code, Some(e.message)))
|
||||
.unwrap_or((None, None));
|
||||
return if matches!(code.as_deref(), Some("index_already_exists")) {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(trc::StoreEvent::MeilisearchError
|
||||
.reason("Meilisearch task failed.")
|
||||
.id(task_uid)
|
||||
.code(code)
|
||||
.details(message))
|
||||
};
|
||||
}
|
||||
TaskStatus::Canceled => {
|
||||
return Err(trc::StoreEvent::MeilisearchError
|
||||
.reason("Meilisearch task was canceled")
|
||||
.id(task_uid));
|
||||
}
|
||||
TaskStatus::Enqueued | TaskStatus::Processing => {
|
||||
loop_count += 1;
|
||||
tokio::time::sleep(self.task_poll_interval).await;
|
||||
}
|
||||
TaskStatus::Unknown => {
|
||||
return Err(trc::StoreEvent::MeilisearchError
|
||||
.reason("Meilisearch task returned an unknown status")
|
||||
.id(task_uid)
|
||||
.details(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let err = trc::StoreEvent::MeilisearchError
|
||||
.reason("Timed out waiting for Meilisearch task")
|
||||
.id(task_uid);
|
||||
|
||||
Err(if self.task_fail_on_timeout {
|
||||
err
|
||||
} else {
|
||||
err.ctx(
|
||||
trc::Key::NextRetry,
|
||||
now().saturating_add(UNCONFIRMED_TASK_RECHECK_DELAY),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn assert_success(response: Result<Response, Error>) -> trc::Result<Response> {
|
||||
match response {
|
||||
Ok(response) => {
|
||||
let status = response.status();
|
||||
if status.is_success() {
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(trc::StoreEvent::MeilisearchError
|
||||
.reason(response.text().await.unwrap_or_default())
|
||||
.ctx(trc::Key::Code, status.as_u16()))
|
||||
}
|
||||
}
|
||||
Err(err) => Err(trc::StoreEvent::MeilisearchError.reason(err)),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use std::time::Duration;
|
||||
|
||||
pub mod main;
|
||||
pub mod search;
|
||||
|
||||
pub struct MeiliSearchStore {
|
||||
client: Client,
|
||||
url: String,
|
||||
task_poll_interval: Duration,
|
||||
task_poll_retries: usize,
|
||||
task_fail_on_timeout: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct TaskUid {
|
||||
#[serde(rename = "taskUid")]
|
||||
pub task_uid: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TaskError {
|
||||
message: String,
|
||||
#[serde(default)]
|
||||
code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Task {
|
||||
//#[serde(rename = "uid")]
|
||||
//uid: u64,
|
||||
status: TaskStatus,
|
||||
#[serde(default)]
|
||||
error: Option<TaskError>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum TaskStatus {
|
||||
Enqueued,
|
||||
Processing,
|
||||
Succeeded,
|
||||
Failed,
|
||||
Canceled,
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MeiliSearchResponse {
|
||||
hits: Vec<MeiliHit>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MeiliDocumentsResponse {
|
||||
results: Vec<MeiliHit>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MeiliHit {
|
||||
id: u64,
|
||||
}
|
||||
@@ -0,0 +1,561 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
backend::meili::{
|
||||
MeiliDocumentsResponse, MeiliSearchResponse, MeiliSearchStore,
|
||||
main::{MAX_TOTAL_HITS, assert_success},
|
||||
},
|
||||
search::*,
|
||||
write::SearchIndex,
|
||||
};
|
||||
use ahash::AHashSet;
|
||||
use serde_json::{Map, Value, json};
|
||||
use std::fmt::{Display, Write};
|
||||
|
||||
const MAX_SEARCH_RESULTS: usize = 10_000;
|
||||
|
||||
impl MeiliSearchStore {
|
||||
pub async fn index(&self, documents: Vec<IndexDocument>) -> trc::Result<()> {
|
||||
let mut index_documents: [String; 5] = [
|
||||
String::new(),
|
||||
String::new(),
|
||||
String::new(),
|
||||
String::new(),
|
||||
String::new(),
|
||||
];
|
||||
|
||||
for document in documents {
|
||||
let request = &mut index_documents[document.index.array_pos()];
|
||||
if !request.is_empty() {
|
||||
request.push(',');
|
||||
} else {
|
||||
request.reserve(1024);
|
||||
request.push('[');
|
||||
}
|
||||
|
||||
json_serialize(request, &document);
|
||||
}
|
||||
|
||||
for (mut payload, index) in index_documents.into_iter().zip([
|
||||
SearchIndex::Email,
|
||||
SearchIndex::Calendar,
|
||||
SearchIndex::Contacts,
|
||||
SearchIndex::Tracing,
|
||||
SearchIndex::File,
|
||||
]) {
|
||||
if payload.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
payload.push(']');
|
||||
|
||||
let response = assert_success(
|
||||
self.client
|
||||
.put(format!(
|
||||
"{}/indexes/{}/documents",
|
||||
self.url,
|
||||
index.index_name()
|
||||
))
|
||||
.body(payload)
|
||||
.send()
|
||||
.await,
|
||||
)
|
||||
.await?;
|
||||
self.wait_for_task(response).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn query<R: SearchDocumentId>(
|
||||
&self,
|
||||
index: SearchIndex,
|
||||
filters: &[SearchFilter],
|
||||
sort: &[SearchComparator],
|
||||
) -> trc::Result<Vec<R>> {
|
||||
let filter_group = build_query(filters);
|
||||
|
||||
if filter_group.q.is_empty() && sort.is_empty() {
|
||||
return self.fetch_documents(index, &filter_group.filter).await;
|
||||
}
|
||||
|
||||
let mut body = Map::new();
|
||||
body.insert("limit".to_string(), Value::from(MAX_SEARCH_RESULTS));
|
||||
body.insert(
|
||||
"attributesToRetrieve".to_string(),
|
||||
Value::Array(vec![Value::String("id".to_string())]),
|
||||
);
|
||||
|
||||
if !filter_group.filter.is_empty() {
|
||||
body.insert("filter".to_string(), Value::String(filter_group.filter));
|
||||
}
|
||||
|
||||
if !filter_group.q.is_empty() {
|
||||
body.insert("q".to_string(), Value::String(filter_group.q));
|
||||
body.insert(
|
||||
"matchingStrategy".to_string(),
|
||||
Value::String("all".to_string()),
|
||||
);
|
||||
|
||||
if !filter_group.search_on.is_empty() {
|
||||
body.insert(
|
||||
"attributesToSearchOn".to_string(),
|
||||
Value::Array(
|
||||
filter_group
|
||||
.search_on
|
||||
.into_iter()
|
||||
.map(|field| Value::String(field.to_string()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !sort.is_empty() {
|
||||
let sort_arr: Vec<Value> = sort
|
||||
.iter()
|
||||
.filter_map(|comp| match comp {
|
||||
SearchComparator::Field { field, ascending } => Some(Value::String(format!(
|
||||
"{}:{}",
|
||||
field.field_name(),
|
||||
if *ascending { "asc" } else { "desc" }
|
||||
))),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
if !sort_arr.is_empty() {
|
||||
body.insert("sort".to_string(), Value::Array(sort_arr));
|
||||
}
|
||||
}
|
||||
|
||||
let url = format!("{}/indexes/{}/search", self.url, index.index_name());
|
||||
let mut results = Vec::new();
|
||||
let mut offset = 0;
|
||||
|
||||
loop {
|
||||
body.insert("offset".to_string(), Value::from(offset));
|
||||
|
||||
let resp = assert_success(
|
||||
self.client
|
||||
.post(&url)
|
||||
.body(Value::Object(body.clone()).to_string())
|
||||
.send()
|
||||
.await,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let text = resp
|
||||
.text()
|
||||
.await
|
||||
.map_err(|err| trc::StoreEvent::MeilisearchError.reason(err))?;
|
||||
|
||||
let hits = serde_json::from_str::<MeiliSearchResponse>(&text)
|
||||
.map_err(|err| {
|
||||
trc::StoreEvent::MeilisearchError
|
||||
.reason(err)
|
||||
.details(text.clone())
|
||||
})?
|
||||
.hits;
|
||||
|
||||
let total = hits.len();
|
||||
results.extend(hits.into_iter().map(|hit| R::from_u64(hit.id)));
|
||||
|
||||
if total < MAX_SEARCH_RESULTS {
|
||||
break;
|
||||
}
|
||||
|
||||
offset += total;
|
||||
|
||||
if offset >= MAX_TOTAL_HITS as usize {
|
||||
trc::event!(
|
||||
Store(trc::StoreEvent::MeilisearchError),
|
||||
Reason = "Search results were truncated",
|
||||
Collection = index.index_name(),
|
||||
Total = offset,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
async fn fetch_documents<R: SearchDocumentId>(
|
||||
&self,
|
||||
index: SearchIndex,
|
||||
filter: &str,
|
||||
) -> trc::Result<Vec<R>> {
|
||||
let url = format!(
|
||||
"{}/indexes/{}/documents/fetch",
|
||||
self.url,
|
||||
index.index_name()
|
||||
);
|
||||
let mut results = Vec::new();
|
||||
let mut offset = 0;
|
||||
|
||||
loop {
|
||||
let mut body = Map::new();
|
||||
body.insert("limit".to_string(), Value::from(MAX_SEARCH_RESULTS));
|
||||
body.insert("offset".to_string(), Value::from(offset));
|
||||
body.insert(
|
||||
"fields".to_string(),
|
||||
Value::Array(vec![Value::String("id".to_string())]),
|
||||
);
|
||||
|
||||
if !filter.is_empty() {
|
||||
body.insert("filter".to_string(), Value::String(filter.to_string()));
|
||||
}
|
||||
|
||||
let resp = assert_success(
|
||||
self.client
|
||||
.post(&url)
|
||||
.body(Value::Object(body).to_string())
|
||||
.send()
|
||||
.await,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let text = resp
|
||||
.text()
|
||||
.await
|
||||
.map_err(|err| trc::StoreEvent::MeilisearchError.reason(err))?;
|
||||
|
||||
let documents = serde_json::from_str::<MeiliDocumentsResponse>(&text)
|
||||
.map_err(|err| {
|
||||
trc::StoreEvent::MeilisearchError
|
||||
.reason(err)
|
||||
.details(text.clone())
|
||||
})?
|
||||
.results;
|
||||
|
||||
let total = documents.len();
|
||||
results.extend(documents.into_iter().map(|hit| R::from_u64(hit.id)));
|
||||
|
||||
if total < MAX_SEARCH_RESULTS {
|
||||
break;
|
||||
}
|
||||
|
||||
offset += total;
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
pub async fn unindex(&self, filter: SearchQuery) -> trc::Result<u64> {
|
||||
let filter_group = build_query(&filter.filters);
|
||||
|
||||
if filter_group.filter.is_empty() {
|
||||
return Err(trc::StoreEvent::MeilisearchError.reason(
|
||||
"Meilisearch delete-by-filter requires structured (non-text) filters only",
|
||||
));
|
||||
}
|
||||
|
||||
let url = format!(
|
||||
"{}/indexes/{}/documents/delete",
|
||||
self.url,
|
||||
filter.index.index_name()
|
||||
);
|
||||
|
||||
let response = assert_success(
|
||||
self.client
|
||||
.post(url)
|
||||
.body(json!({ "filter": filter_group.filter }).to_string())
|
||||
.send()
|
||||
.await,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.wait_for_task(response).await?;
|
||||
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct FilterGroup {
|
||||
q: String,
|
||||
filter: String,
|
||||
search_on: AHashSet<&'static str>,
|
||||
}
|
||||
|
||||
fn build_query(filters: &[SearchFilter]) -> FilterGroup {
|
||||
if filters.is_empty() {
|
||||
return FilterGroup::default();
|
||||
}
|
||||
let mut operator_stack = Vec::new();
|
||||
let mut operator = &SearchFilter::And;
|
||||
let mut is_first = true;
|
||||
let mut filter = String::new();
|
||||
let mut queries = AHashSet::new();
|
||||
let mut search_on = AHashSet::new();
|
||||
|
||||
for f in filters {
|
||||
match f {
|
||||
SearchFilter::Operator { field, op, value } => {
|
||||
if field.is_text() && matches!(op, SearchOperator::Equal | SearchOperator::Contains)
|
||||
{
|
||||
let value = match value {
|
||||
SearchValue::Text { value, .. } => value,
|
||||
_ => {
|
||||
debug_assert!(
|
||||
false,
|
||||
"Text field search with non-text value is not supported"
|
||||
);
|
||||
""
|
||||
}
|
||||
};
|
||||
|
||||
search_on.insert(field.field_name());
|
||||
|
||||
if matches!(op, SearchOperator::Equal) {
|
||||
queries.insert(format!("{value:?}"));
|
||||
} else {
|
||||
for token in value.split_whitespace() {
|
||||
queries.insert(token.to_string());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !filter.is_empty() && !filter.ends_with('(') {
|
||||
match operator {
|
||||
SearchFilter::And => filter.push_str(" AND "),
|
||||
SearchFilter::Or => filter.push_str(" OR "),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
match value {
|
||||
SearchValue::Text { value, .. } => {
|
||||
filter.push_str(field.field_name());
|
||||
filter.push(' ');
|
||||
op.write_meli_op(&mut filter, format!("{value:?}"));
|
||||
}
|
||||
SearchValue::KeyValues(kv) => {
|
||||
let (key, value) = kv.iter().next().unwrap();
|
||||
filter.push_str(field.field_name());
|
||||
filter.push('.');
|
||||
filter.push_str(key);
|
||||
filter.push(' ');
|
||||
op.write_meli_op(&mut filter, format!("{value:?}"));
|
||||
}
|
||||
SearchValue::Int(v) => {
|
||||
filter.push_str(field.field_name());
|
||||
filter.push(' ');
|
||||
op.write_meli_op(&mut filter, v);
|
||||
}
|
||||
SearchValue::Uint(v) => {
|
||||
filter.push_str(field.field_name());
|
||||
filter.push(' ');
|
||||
op.write_meli_op(&mut filter, v);
|
||||
}
|
||||
SearchValue::Boolean(v) => {
|
||||
filter.push_str(field.field_name());
|
||||
filter.push(' ');
|
||||
op.write_meli_op(&mut filter, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SearchFilter::And | SearchFilter::Or => {
|
||||
if !filter.is_empty() && !filter.ends_with('(') {
|
||||
match operator {
|
||||
SearchFilter::And => filter.push_str(" AND "),
|
||||
SearchFilter::Or => filter.push_str(" OR "),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
operator_stack.push((operator, is_first));
|
||||
operator = f;
|
||||
is_first = true;
|
||||
filter.push('(');
|
||||
}
|
||||
SearchFilter::Not => {
|
||||
if !filter.is_empty() && !filter.ends_with('(') {
|
||||
match operator {
|
||||
SearchFilter::And => filter.push_str(" AND "),
|
||||
SearchFilter::Or => filter.push_str(" OR "),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
operator_stack.push((operator, is_first));
|
||||
operator = &SearchFilter::And;
|
||||
is_first = true;
|
||||
filter.push_str("NOT (");
|
||||
}
|
||||
SearchFilter::End => {
|
||||
let p = operator_stack.pop().unwrap_or((&SearchFilter::And, true));
|
||||
operator = p.0;
|
||||
is_first = p.1;
|
||||
|
||||
if !filter.ends_with('(') {
|
||||
filter.push(')');
|
||||
} else {
|
||||
filter.pop();
|
||||
if filter.ends_with("NOT ") {
|
||||
let len = filter.len();
|
||||
filter.truncate(len - 4);
|
||||
}
|
||||
if filter.ends_with(" AND ") {
|
||||
let len = filter.len();
|
||||
filter.truncate(len - 5);
|
||||
is_first = true;
|
||||
} else if filter.ends_with(" OR ") {
|
||||
let len = filter.len();
|
||||
filter.truncate(len - 4);
|
||||
is_first = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
SearchFilter::DocumentSet(_) => {
|
||||
debug_assert!(false, "DocumentSet filters are not supported")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut q = String::new();
|
||||
if !queries.is_empty() {
|
||||
for (idx, term) in queries.into_iter().enumerate() {
|
||||
if idx > 0 {
|
||||
q.push(' ');
|
||||
}
|
||||
q.push_str(&term);
|
||||
}
|
||||
}
|
||||
|
||||
FilterGroup {
|
||||
q,
|
||||
filter,
|
||||
search_on,
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchOperator {
|
||||
fn write_meli_op(&self, query: &mut String, value: impl Display) {
|
||||
match self {
|
||||
SearchOperator::LowerThan => {
|
||||
let _ = write!(query, "< {value}");
|
||||
}
|
||||
SearchOperator::LowerEqualThan => {
|
||||
let _ = write!(query, "<= {value}");
|
||||
}
|
||||
SearchOperator::GreaterThan => {
|
||||
let _ = write!(query, "> {value}");
|
||||
}
|
||||
SearchOperator::GreaterEqualThan => {
|
||||
let _ = write!(query, ">= {value}");
|
||||
}
|
||||
SearchOperator::Equal | SearchOperator::Contains => {
|
||||
let _ = write!(query, "= {value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn json_serialize(request: &mut String, document: &IndexDocument) {
|
||||
let mut id = 0u64;
|
||||
let mut is_first = true;
|
||||
request.push('{');
|
||||
for (k, v) in document.fields.iter() {
|
||||
match k {
|
||||
SearchField::AccountId => {
|
||||
if let SearchValue::Uint(account_id) = v {
|
||||
id |= account_id << 32;
|
||||
}
|
||||
}
|
||||
SearchField::DocumentId => {
|
||||
if let SearchValue::Uint(doc_id) = v {
|
||||
id |= doc_id;
|
||||
}
|
||||
}
|
||||
SearchField::Id => {
|
||||
if let SearchValue::Uint(doc_id) = v {
|
||||
id = *doc_id;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if !is_first {
|
||||
request.push(',');
|
||||
} else {
|
||||
is_first = false;
|
||||
}
|
||||
|
||||
let _ = write!(request, "{:?}:", k.field_name());
|
||||
match v {
|
||||
SearchValue::Text { value, .. } => {
|
||||
json_serialize_str(request, value);
|
||||
}
|
||||
SearchValue::KeyValues(map) => {
|
||||
request.push('{');
|
||||
for (i, (key, value)) in map.iter().enumerate() {
|
||||
if i > 0 {
|
||||
request.push(',');
|
||||
}
|
||||
json_serialize_str(request, key);
|
||||
request.push(':');
|
||||
json_serialize_str(request, value);
|
||||
}
|
||||
request.push('}');
|
||||
}
|
||||
SearchValue::Int(v) => {
|
||||
let _ = write!(request, "{}", v);
|
||||
}
|
||||
SearchValue::Uint(v) => {
|
||||
let _ = write!(request, "{}", v);
|
||||
}
|
||||
SearchValue::Boolean(v) => {
|
||||
let _ = write!(request, "{}", v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*if id == 0 {
|
||||
debug_assert!(false, "Document is missing required ID fields");
|
||||
}*/
|
||||
|
||||
let _ = write!(request, ",\"id\":{id}}}");
|
||||
}
|
||||
|
||||
fn json_serialize_str(request: &mut String, value: &str) {
|
||||
request.push('"');
|
||||
for c in value.chars() {
|
||||
match c {
|
||||
'"' => request.push_str("\\\""),
|
||||
'\\' => request.push_str("\\\\"),
|
||||
'\n' => request.push_str("\\n"),
|
||||
'\r' => request.push_str("\\r"),
|
||||
'\t' => request.push_str("\\t"),
|
||||
'\u{0008}' => request.push_str("\\b"), // backspace
|
||||
'\u{000C}' => request.push_str("\\f"), // form feed
|
||||
_ => {
|
||||
if !c.is_control() {
|
||||
request.push(c);
|
||||
} else {
|
||||
let _ = write!(request, "\\u{:04x}", c as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
request.push('"');
|
||||
}
|
||||
|
||||
impl SearchIndex {
|
||||
#[inline(always)]
|
||||
fn array_pos(&self) -> usize {
|
||||
match self {
|
||||
SearchIndex::Email => 0,
|
||||
SearchIndex::Calendar => 1,
|
||||
SearchIndex::Contacts => 2,
|
||||
SearchIndex::Tracing => 3,
|
||||
SearchIndex::File => 4,
|
||||
SearchIndex::InMemory => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{InMemoryStore, LookupStores, Value, registry::bootstrap::Bootstrap};
|
||||
use ahash::AHashMap;
|
||||
use registry::schema::structs;
|
||||
use utils::glob::{GlobMap, GlobSet};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum StaticMemoryStore {
|
||||
Map(GlobMap<Value<'static>>),
|
||||
Set(GlobSet),
|
||||
}
|
||||
|
||||
impl LookupStores {
|
||||
pub async fn parse_static(&mut self, bp: &mut Bootstrap) {
|
||||
let mut lookups = AHashMap::new();
|
||||
|
||||
for lookup in bp.list_infallible::<structs::MemoryLookupKeyValue>().await {
|
||||
if let StaticMemoryStore::Map(map) = lookups
|
||||
.entry(lookup.object.namespace)
|
||||
.or_insert_with(|| StaticMemoryStore::Map(Default::default()))
|
||||
{
|
||||
if lookup.object.is_glob_pattern {
|
||||
map.insert_pattern(&lookup.object.key, Value::from(lookup.object.value));
|
||||
} else {
|
||||
map.insert_entry(lookup.object.key, Value::from(lookup.object.value));
|
||||
}
|
||||
} else {
|
||||
bp.build_warning(
|
||||
lookup.id,
|
||||
"Memory lookup has mixed types (key-value and set)",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for lookup in bp.list_infallible::<structs::MemoryLookupKey>().await {
|
||||
if let StaticMemoryStore::Set(set) = lookups
|
||||
.entry(lookup.object.namespace)
|
||||
.or_insert_with(|| StaticMemoryStore::Set(Default::default()))
|
||||
{
|
||||
if lookup.object.is_glob_pattern {
|
||||
set.insert_pattern(&lookup.object.key);
|
||||
} else {
|
||||
set.insert_entry(lookup.object.key);
|
||||
}
|
||||
} else {
|
||||
bp.build_warning(
|
||||
lookup.id,
|
||||
"Memory lookup has mixed types (key-value and set)",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (namespace, store) in lookups {
|
||||
self.stores.insert(
|
||||
namespace.into_boxed_str(),
|
||||
InMemoryStore::Static(store.into()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
#[cfg(feature = "azure")]
|
||||
pub mod azure;
|
||||
pub mod elastic;
|
||||
pub mod ephemeral;
|
||||
#[cfg(feature = "foundation")]
|
||||
pub mod foundationdb;
|
||||
pub mod fs;
|
||||
pub mod http;
|
||||
pub mod meili;
|
||||
pub mod memory;
|
||||
#[cfg(feature = "mysql")]
|
||||
pub mod mysql;
|
||||
#[cfg(feature = "postgres")]
|
||||
pub mod postgres;
|
||||
#[cfg(feature = "redis")]
|
||||
pub mod redis;
|
||||
#[cfg(feature = "rocks")]
|
||||
pub mod rocksdb;
|
||||
#[cfg(feature = "s3")]
|
||||
pub mod s3;
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub mod sqlite;
|
||||
|
||||
|
||||
pub const MAX_TOKEN_LENGTH: usize = (u8::MAX >> 1) as usize;
|
||||
pub const MAX_TOKEN_MASK: usize = MAX_TOKEN_LENGTH - 1;
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn deserialize_i64_le(key: &[u8], bytes: &[u8]) -> trc::Result<i64> {
|
||||
Ok(i64::from_le_bytes(bytes[..].try_into().map_err(|_| {
|
||||
trc::Error::corrupted_key(key, bytes.into(), trc::location!())
|
||||
})?))
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use mysql_async::prelude::Queryable;
|
||||
|
||||
use super::{MysqlStore, into_error};
|
||||
|
||||
impl MysqlStore {
|
||||
pub(crate) async fn get_blob(
|
||||
&self,
|
||||
key: &[u8],
|
||||
range: Range<usize>,
|
||||
) -> trc::Result<Option<Vec<u8>>> {
|
||||
let mut conn = self.conn_pool.get_conn().await.map_err(into_error)?;
|
||||
let s = conn
|
||||
.prep("SELECT v FROM t WHERE k = ?")
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
conn.exec_first::<Vec<u8>, _, _>(&s, (key,))
|
||||
.await
|
||||
.map(|bytes| {
|
||||
if range.start == 0 && range.end == usize::MAX {
|
||||
bytes
|
||||
} else {
|
||||
bytes.map(|bytes| {
|
||||
bytes
|
||||
.get(range.start..std::cmp::min(bytes.len(), range.end))
|
||||
.unwrap_or_default()
|
||||
.to_vec()
|
||||
})
|
||||
}
|
||||
})
|
||||
.map_err(into_error)
|
||||
}
|
||||
|
||||
pub(crate) async fn put_blob(&self, key: &[u8], data: &[u8]) -> trc::Result<()> {
|
||||
let mut conn = self.conn_pool.get_conn().await.map_err(into_error)?;
|
||||
let s = conn
|
||||
.prep("INSERT INTO t (k, v) VALUES (?, ?) ON DUPLICATE KEY UPDATE v = VALUES(v)")
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
conn.exec_drop(&s, (key, data))
|
||||
.await
|
||||
.map_err(into_error)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_blob(&self, key: &[u8]) -> trc::Result<bool> {
|
||||
let mut conn = self.conn_pool.get_conn().await.map_err(into_error)?;
|
||||
let s = conn
|
||||
.prep("DELETE FROM t WHERE k = ?")
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
conn.exec_iter(&s, (key,))
|
||||
.await
|
||||
.map_err(into_error)
|
||||
.map(|hits| hits.affected_rows() > 0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use mysql_async::{Params, Row, prelude::Queryable};
|
||||
|
||||
use crate::{IntoRows, QueryResult, QueryType, Value};
|
||||
|
||||
use super::{MysqlStore, into_error};
|
||||
|
||||
impl MysqlStore {
|
||||
pub(crate) async fn sql_query<T: QueryResult>(
|
||||
&self,
|
||||
query: &str,
|
||||
params: &[Value<'_>],
|
||||
) -> trc::Result<T> {
|
||||
let mut conn = self.conn_pool.get_conn().await.map_err(into_error)?;
|
||||
let s = conn.prep(query).await.map_err(into_error)?;
|
||||
let params = Params::Positional(params.iter().map(Into::into).collect());
|
||||
|
||||
match T::query_type() {
|
||||
QueryType::Execute => conn.exec_drop(s, params).await.map_or_else(
|
||||
|e| Err(into_error(e)),
|
||||
|_| Ok(T::from_exec(conn.affected_rows() as usize)),
|
||||
),
|
||||
QueryType::Exists => conn
|
||||
.exec_first::<Row, _, _>(s, params)
|
||||
.await
|
||||
.map_or_else(|e| Err(into_error(e)), |r| Ok(T::from_exists(r.is_some()))),
|
||||
QueryType::QueryOne => conn
|
||||
.exec_first::<Row, _, _>(s, params)
|
||||
.await
|
||||
.map_or_else(|e| Err(into_error(e)), |r| Ok(T::from_query_one(r))),
|
||||
QueryType::QueryAll => conn
|
||||
.exec::<Row, _, _>(s, params)
|
||||
.await
|
||||
.map_or_else(|e| Err(into_error(e)), |r| Ok(T::from_query_all(r))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::Value<'_>> for mysql_async::Value {
|
||||
fn from(value: crate::Value) -> Self {
|
||||
match value {
|
||||
crate::Value::Integer(i) => mysql_async::Value::Int(i),
|
||||
crate::Value::Bool(b) => mysql_async::Value::Int(b as i64),
|
||||
crate::Value::Float(f) => mysql_async::Value::Double(f),
|
||||
crate::Value::Text(t) => mysql_async::Value::Bytes(t.into_owned().into_bytes()),
|
||||
crate::Value::Blob(b) => mysql_async::Value::Bytes(b.into_owned()),
|
||||
crate::Value::Null => mysql_async::Value::NULL,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mysql_async::Value> for crate::Value<'static> {
|
||||
fn from(value: mysql_async::Value) -> Self {
|
||||
match value {
|
||||
mysql_async::Value::Int(i) => Self::Integer(i),
|
||||
mysql_async::Value::UInt(i) => Self::Integer(i as i64),
|
||||
mysql_async::Value::Double(f) => Self::Float(f),
|
||||
mysql_async::Value::Bytes(b) => String::from_utf8(b).map_or_else(
|
||||
|e| Self::Blob(e.into_bytes().into()),
|
||||
|s| Self::Text(s.into()),
|
||||
),
|
||||
mysql_async::Value::NULL => Self::Null,
|
||||
mysql_async::Value::Float(f) => Self::Float(f as f64),
|
||||
mysql_async::Value::Date(_, _, _, _, _, _, _)
|
||||
| mysql_async::Value::Time(_, _, _, _, _, _) => Self::Text(value.as_sql(true).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoRows for Vec<mysql_async::Row> {
|
||||
fn into_rows(self) -> crate::Rows {
|
||||
crate::Rows {
|
||||
rows: self
|
||||
.into_iter()
|
||||
.map(|r| crate::Row {
|
||||
values: r
|
||||
.unwrap_raw()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_named_rows(self) -> crate::NamedRows {
|
||||
crate::NamedRows {
|
||||
names: self
|
||||
.first()
|
||||
.map(|r| r.columns().iter().map(|c| c.name_str().into()).collect())
|
||||
.unwrap_or_default(),
|
||||
rows: self
|
||||
.into_iter()
|
||||
.map(|r| crate::Row {
|
||||
values: r
|
||||
.unwrap_raw()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_row(self) -> Option<crate::Row> {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoRows for Option<mysql_async::Row> {
|
||||
fn into_row(self) -> Option<crate::Row> {
|
||||
self.map(|row| crate::Row {
|
||||
values: row
|
||||
.unwrap_raw()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn into_rows(self) -> crate::Rows {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn into_named_rows(self) -> crate::NamedRows {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{MysqlStore, into_error};
|
||||
use crate::{
|
||||
backend::mysql::MysqlSearchField,
|
||||
search::{
|
||||
CalendarSearchField, ContactSearchField, EmailSearchField, SearchableField,
|
||||
TracingSearchField,
|
||||
},
|
||||
*,
|
||||
};
|
||||
use ::registry::schema::structs;
|
||||
use mysql_async::{
|
||||
Conn, OptsBuilder, Pool, PoolConstraints, PoolOpts, SslOpts, prelude::Queryable,
|
||||
};
|
||||
|
||||
impl MysqlStore {
|
||||
pub async fn open(config: structs::MySqlStore) -> Result<Store, String> {
|
||||
let mut opts = OptsBuilder::default()
|
||||
.ip_or_hostname(config.host)
|
||||
.user(config.auth_username)
|
||||
.pass(config.auth_secret.secret().await?.map(|v| v.into_owned()))
|
||||
.db_name(Some(config.database))
|
||||
.max_allowed_packet(config.max_allowed_packet.map(|v| v as usize))
|
||||
.wait_timeout(config.timeout.map(|t| t.as_secs() as usize))
|
||||
.client_found_rows(true)
|
||||
.tcp_port(config.port as u16);
|
||||
|
||||
if config.use_tls {
|
||||
opts = opts.ssl_opts(Some(
|
||||
SslOpts::default()
|
||||
.with_danger_accept_invalid_certs(config.allow_invalid_certs)
|
||||
.with_danger_skip_domain_validation(config.allow_invalid_certs),
|
||||
));
|
||||
}
|
||||
|
||||
// Configure connection pool
|
||||
let mut pool_min = PoolConstraints::default().min();
|
||||
let mut pool_max = PoolConstraints::default().max();
|
||||
if let Some(n_size) = config.pool_min_connections {
|
||||
pool_min = n_size as usize;
|
||||
}
|
||||
if let Some(n_size) = config.pool_max_connections {
|
||||
pool_max = n_size as usize;
|
||||
}
|
||||
opts = opts.pool_opts(
|
||||
PoolOpts::default().with_constraints(PoolConstraints::new(pool_min, pool_max).unwrap()),
|
||||
);
|
||||
|
||||
let mut replicas = vec![];
|
||||
for replica in config.read_replicas {
|
||||
replicas.push(Store::MySQL(Arc::new(MysqlStore {
|
||||
conn_pool: Pool::new(
|
||||
opts.clone()
|
||||
.ip_or_hostname(replica.host)
|
||||
.user(replica.auth_username)
|
||||
.pass(replica.auth_secret.secret().await?.map(|v| v.into_owned()))
|
||||
.db_name(Some(replica.database))
|
||||
.tcp_port(replica.port as u16),
|
||||
),
|
||||
})))
|
||||
}
|
||||
|
||||
let primary = Store::MySQL(Arc::new(MysqlStore {
|
||||
conn_pool: Pool::new(opts),
|
||||
}));
|
||||
|
||||
|
||||
Ok(primary)
|
||||
}
|
||||
|
||||
pub(crate) async fn create_storage_tables(&self) -> trc::Result<()> {
|
||||
let mut conn = self.conn_pool.get_conn().await.map_err(into_error)?;
|
||||
|
||||
for table in [
|
||||
SUBSPACE_ACL,
|
||||
SUBSPACE_TASK_QUEUE,
|
||||
SUBSPACE_DELETED_ITEMS,
|
||||
SUBSPACE_SPAM_SAMPLES,
|
||||
SUBSPACE_BLOB_LINK,
|
||||
SUBSPACE_IN_MEMORY_VALUE,
|
||||
SUBSPACE_PROPERTY,
|
||||
SUBSPACE_REGISTRY,
|
||||
SUBSPACE_REGISTRY_PK,
|
||||
SUBSPACE_DIRECTORY,
|
||||
SUBSPACE_QUEUE_MESSAGE,
|
||||
SUBSPACE_QUEUE_EVENT,
|
||||
SUBSPACE_REPORT_OUT,
|
||||
SUBSPACE_REPORT_IN,
|
||||
SUBSPACE_LOGS,
|
||||
SUBSPACE_TELEMETRY_SPAN,
|
||||
SUBSPACE_TELEMETRY_METRIC,
|
||||
] {
|
||||
let table = char::from(table);
|
||||
conn.query_drop(format!(
|
||||
"CREATE TABLE IF NOT EXISTS {table} (
|
||||
k VARBINARY(255) NOT NULL,
|
||||
v MEDIUMBLOB NOT NULL,
|
||||
PRIMARY KEY (k)
|
||||
) ENGINE=InnoDB"
|
||||
))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
}
|
||||
|
||||
conn.query_drop(format!(
|
||||
"CREATE TABLE IF NOT EXISTS {} (
|
||||
k VARBINARY(255) NOT NULL,
|
||||
v LONGBLOB NOT NULL,
|
||||
PRIMARY KEY (k)
|
||||
) ENGINE=InnoDB",
|
||||
char::from(SUBSPACE_BLOBS),
|
||||
))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
|
||||
for table in [SUBSPACE_INDEXES, SUBSPACE_REGISTRY_IDX] {
|
||||
let table = char::from(table);
|
||||
conn.query_drop(format!(
|
||||
"CREATE TABLE IF NOT EXISTS {table} (
|
||||
k BLOB,
|
||||
PRIMARY KEY (k(400))
|
||||
) ENGINE=InnoDB"
|
||||
))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
}
|
||||
|
||||
for table in [SUBSPACE_COUNTER, SUBSPACE_QUOTA, SUBSPACE_IN_MEMORY_COUNTER] {
|
||||
conn.query_drop(format!(
|
||||
"CREATE TABLE IF NOT EXISTS {} (
|
||||
k VARBINARY(255) NOT NULL,
|
||||
v BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (k)
|
||||
) ENGINE=InnoDB",
|
||||
char::from(table)
|
||||
))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn create_search_tables(&self) -> trc::Result<()> {
|
||||
let mut conn = self.conn_pool.get_conn().await.map_err(into_error)?;
|
||||
|
||||
create_search_tables::<EmailSearchField>(&mut conn).await?;
|
||||
create_search_tables::<CalendarSearchField>(&mut conn).await?;
|
||||
create_search_tables::<ContactSearchField>(&mut conn).await?;
|
||||
//create_search_tables::<FileSearchField>(&mut conn).await?;
|
||||
create_search_tables::<TracingSearchField>(&mut conn).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_search_tables<T: SearchableField + MysqlSearchField + 'static>(
|
||||
conn: &mut Conn,
|
||||
) -> trc::Result<()> {
|
||||
let table_name = T::index().mysql_table();
|
||||
let mut query = format!("CREATE TABLE IF NOT EXISTS {} (", table_name);
|
||||
|
||||
// Add primary key columns
|
||||
let pkeys = T::primary_keys();
|
||||
for pkey in pkeys {
|
||||
query.push_str(&format!("{} {}, ", pkey.column(), pkey.column_type()));
|
||||
}
|
||||
|
||||
// Add other columns
|
||||
for field in T::all_fields() {
|
||||
query.push_str(&format!("{} {}, ", field.column(), field.column_type()));
|
||||
}
|
||||
|
||||
// Add primary key constraint
|
||||
query.push_str("PRIMARY KEY (");
|
||||
for (i, pkey) in pkeys.iter().enumerate() {
|
||||
if i > 0 {
|
||||
query.push_str(", ");
|
||||
}
|
||||
query.push_str(pkey.column());
|
||||
}
|
||||
query.push_str(")) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
|
||||
|
||||
conn.query_drop(&query).await.map_err(into_error)?;
|
||||
|
||||
// Create indexes
|
||||
for field in T::all_fields() {
|
||||
if field.is_text() {
|
||||
let column_name = field.column();
|
||||
let create_index_query = format!(
|
||||
"CREATE FULLTEXT INDEX fts_{table_name}_{column_name} ON {table_name}({column_name})",
|
||||
);
|
||||
|
||||
let _ = conn.query_drop(&create_index_query).await;
|
||||
}
|
||||
|
||||
if field.is_indexed() {
|
||||
let column_name = field.column();
|
||||
let create_index_query = format!(
|
||||
"CREATE INDEX idx_{table_name}_{column_name} ON {table_name}({column_name})",
|
||||
);
|
||||
let _ = conn.query_drop(&create_index_query).await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
search::{
|
||||
CalendarSearchField, ContactSearchField, EmailSearchField, FileSearchField, SearchField,
|
||||
TracingSearchField,
|
||||
},
|
||||
write::SearchIndex,
|
||||
};
|
||||
use mysql_async::Pool;
|
||||
use std::fmt::Display;
|
||||
|
||||
pub mod blob;
|
||||
pub mod lookup;
|
||||
pub mod main;
|
||||
pub mod read;
|
||||
pub mod search;
|
||||
pub mod write;
|
||||
|
||||
pub struct MysqlStore {
|
||||
pub(crate) conn_pool: Pool,
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn into_error(err: impl Display) -> trc::Error {
|
||||
trc::StoreEvent::MysqlError.reason(err)
|
||||
}
|
||||
|
||||
const ER_LOCK_WAIT_TIMEOUT: u16 = 1205;
|
||||
const ER_STATEMENT_TIMEOUT: u16 = 1969;
|
||||
const ER_QUERY_TIMEOUT: u16 = 3024;
|
||||
|
||||
pub(crate) const DELETE_CHUNK_SIZE: usize = 1000;
|
||||
pub(crate) const MIN_DELETE_CHUNK_SIZE: usize = 10;
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn is_timeout_error(err: &mysql_async::Error) -> bool {
|
||||
matches!(err, mysql_async::Error::Server(err)
|
||||
if matches!(
|
||||
err.code,
|
||||
ER_LOCK_WAIT_TIMEOUT | ER_STATEMENT_TIMEOUT | ER_QUERY_TIMEOUT
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
impl SearchIndex {
|
||||
pub fn mysql_table(&self) -> &'static str {
|
||||
match self {
|
||||
SearchIndex::Email => "s_email",
|
||||
SearchIndex::Calendar => "s_cal",
|
||||
SearchIndex::Contacts => "s_card",
|
||||
SearchIndex::File => "s_file",
|
||||
SearchIndex::Tracing => "s_trace",
|
||||
SearchIndex::InMemory => "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait MysqlSearchField {
|
||||
fn column(&self) -> &'static str;
|
||||
fn column_type(&self) -> &'static str;
|
||||
}
|
||||
|
||||
impl MysqlSearchField for EmailSearchField {
|
||||
fn column(&self) -> &'static str {
|
||||
match self {
|
||||
EmailSearchField::From => "fadr",
|
||||
EmailSearchField::To => "tadr",
|
||||
EmailSearchField::Cc => "cc",
|
||||
EmailSearchField::Bcc => "bcc",
|
||||
EmailSearchField::Subject => "subj",
|
||||
EmailSearchField::Body => "body",
|
||||
EmailSearchField::Attachment => "atta",
|
||||
EmailSearchField::ReceivedAt => "rcvd",
|
||||
EmailSearchField::SentAt => "sent",
|
||||
EmailSearchField::Size => "size",
|
||||
EmailSearchField::HasAttachment => "hatt",
|
||||
EmailSearchField::Headers => "hdrs",
|
||||
}
|
||||
}
|
||||
|
||||
fn column_type(&self) -> &'static str {
|
||||
match self {
|
||||
EmailSearchField::ReceivedAt | EmailSearchField::SentAt => "BIGINT",
|
||||
EmailSearchField::Size => "INT",
|
||||
EmailSearchField::HasAttachment => "BOOLEAN",
|
||||
EmailSearchField::Headers => "JSON",
|
||||
EmailSearchField::From => "TEXT",
|
||||
EmailSearchField::To => "TEXT",
|
||||
EmailSearchField::Cc => "TEXT",
|
||||
EmailSearchField::Bcc => "TEXT",
|
||||
EmailSearchField::Subject => "TEXT",
|
||||
EmailSearchField::Body => "MEDIUMTEXT",
|
||||
EmailSearchField::Attachment => "MEDIUMTEXT",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MysqlSearchField for CalendarSearchField {
|
||||
fn column(&self) -> &'static str {
|
||||
match self {
|
||||
CalendarSearchField::Title => "titl",
|
||||
CalendarSearchField::Description => "dscd",
|
||||
CalendarSearchField::Location => "locn",
|
||||
CalendarSearchField::Owner => "ownr",
|
||||
CalendarSearchField::Attendee => "atnd",
|
||||
CalendarSearchField::Start => "strt",
|
||||
CalendarSearchField::Uid => "uid",
|
||||
}
|
||||
}
|
||||
|
||||
fn column_type(&self) -> &'static str {
|
||||
match self {
|
||||
CalendarSearchField::Start => "BIGINT NOT NULL",
|
||||
_ => "TEXT",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MysqlSearchField for ContactSearchField {
|
||||
fn column(&self) -> &'static str {
|
||||
match self {
|
||||
ContactSearchField::Member => "mmbr",
|
||||
ContactSearchField::Name => "name",
|
||||
ContactSearchField::Nickname => "nick",
|
||||
ContactSearchField::Organization => "orgn",
|
||||
ContactSearchField::Email => "eml",
|
||||
ContactSearchField::Phone => "phon",
|
||||
ContactSearchField::OnlineService => "olsv",
|
||||
ContactSearchField::Address => "addr",
|
||||
ContactSearchField::Note => "note",
|
||||
ContactSearchField::Kind => "kind",
|
||||
ContactSearchField::Uid => "uid",
|
||||
}
|
||||
}
|
||||
|
||||
fn column_type(&self) -> &'static str {
|
||||
match self {
|
||||
ContactSearchField::Kind | ContactSearchField::Uid => "TEXT",
|
||||
_ => "TEXT",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MysqlSearchField for FileSearchField {
|
||||
fn column(&self) -> &'static str {
|
||||
match self {
|
||||
FileSearchField::Name => "name",
|
||||
FileSearchField::Content => "body",
|
||||
}
|
||||
}
|
||||
|
||||
fn column_type(&self) -> &'static str {
|
||||
match self {
|
||||
FileSearchField::Name => "TEXT",
|
||||
FileSearchField::Content => "MEDIUMTEXT",
|
||||
}
|
||||
}
|
||||
}
|
||||
impl MysqlSearchField for TracingSearchField {
|
||||
fn column(&self) -> &'static str {
|
||||
match self {
|
||||
TracingSearchField::QueueId => "qid",
|
||||
TracingSearchField::EventType => "etyp",
|
||||
TracingSearchField::Keywords => "kwds",
|
||||
}
|
||||
}
|
||||
|
||||
fn column_type(&self) -> &'static str {
|
||||
match self {
|
||||
TracingSearchField::EventType => "BIGINT",
|
||||
TracingSearchField::QueueId => "BIGINT",
|
||||
TracingSearchField::Keywords => "TEXT",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MysqlSearchField for SearchField {
|
||||
fn column(&self) -> &'static str {
|
||||
match self {
|
||||
SearchField::AccountId => "accid",
|
||||
SearchField::DocumentId => "docid",
|
||||
SearchField::Id => "id",
|
||||
SearchField::Email(field) => field.column(),
|
||||
SearchField::Calendar(field) => field.column(),
|
||||
SearchField::Contact(field) => field.column(),
|
||||
SearchField::File(field) => field.column(),
|
||||
SearchField::Tracing(field) => field.column(),
|
||||
}
|
||||
}
|
||||
|
||||
fn column_type(&self) -> &'static str {
|
||||
match self {
|
||||
SearchField::AccountId => "INT NOT NULL",
|
||||
SearchField::DocumentId => "INT NOT NULL",
|
||||
SearchField::Id => "BIGINT NOT NULL",
|
||||
SearchField::Email(field) => field.column_type(),
|
||||
SearchField::Calendar(field) => field.column_type(),
|
||||
SearchField::Contact(field) => field.column_type(),
|
||||
SearchField::File(field) => field.column_type(),
|
||||
SearchField::Tracing(field) => field.column_type(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{MysqlStore, into_error, is_timeout_error};
|
||||
use crate::{Deserialize, IterateParams, Key, ValueKey, write::ValueClass};
|
||||
use futures::TryStreamExt;
|
||||
use mysql_async::{Row, prelude::Queryable};
|
||||
|
||||
impl MysqlStore {
|
||||
pub(crate) async fn get_value<U>(&self, key: impl Key) -> trc::Result<Option<U>>
|
||||
where
|
||||
U: Deserialize + 'static,
|
||||
{
|
||||
let mut conn = self.conn_pool.get_conn().await.map_err(into_error)?;
|
||||
let s = conn
|
||||
.prep(format!(
|
||||
"SELECT v FROM {} WHERE k = ?",
|
||||
char::from(key.subspace())
|
||||
))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
let key = key.serialize(0);
|
||||
conn.exec_first::<Vec<u8>, _, _>(&s, (&key,))
|
||||
.await
|
||||
.map_err(into_error)
|
||||
.and_then(|r| {
|
||||
if let Some(r) = r {
|
||||
Ok(Some(U::deserialize_owned_with_key(&key, r)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn key_exists(&self, key: impl Key) -> trc::Result<bool> {
|
||||
let mut conn = self.conn_pool.get_conn().await.map_err(into_error)?;
|
||||
let s = conn
|
||||
.prep(format!(
|
||||
"SELECT 1 FROM {} WHERE k = ?",
|
||||
char::from(key.subspace())
|
||||
))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
let key = key.serialize(0);
|
||||
conn.exec_first::<u8, _, _>(&s, (&key,))
|
||||
.await
|
||||
.map_err(into_error)
|
||||
.map(|r| r.is_some())
|
||||
}
|
||||
|
||||
pub(crate) async fn iterate<T: Key>(
|
||||
&self,
|
||||
params: IterateParams<T>,
|
||||
mut cb: impl for<'x> FnMut(&'x [u8], &'x [u8]) -> trc::Result<bool> + Sync + Send,
|
||||
) -> trc::Result<()> {
|
||||
let mut conn = self.conn_pool.get_conn().await.map_err(into_error)?;
|
||||
let table = char::from(params.begin.subspace());
|
||||
let begin = params.begin.serialize(0);
|
||||
let end = params.end.serialize(0);
|
||||
let keys = if params.values { "k, v" } else { "k" };
|
||||
|
||||
let s = conn
|
||||
.prep(&match (params.first, params.ascending) {
|
||||
(true, true) => {
|
||||
format!(
|
||||
"SELECT {keys} FROM {table} WHERE k >= ? AND k <= ? ORDER BY k ASC LIMIT 1"
|
||||
)
|
||||
}
|
||||
(true, false) => {
|
||||
format!(
|
||||
"SELECT {keys} FROM {table} WHERE k >= ? AND k <= ? ORDER BY k DESC LIMIT 1"
|
||||
)
|
||||
}
|
||||
(false, true) => {
|
||||
format!("SELECT {keys} FROM {table} WHERE k >= ? AND k <= ? ORDER BY k ASC")
|
||||
}
|
||||
(false, false) => {
|
||||
format!("SELECT {keys} FROM {table} WHERE k >= ? AND k <= ? ORDER BY k DESC")
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
let mut from = begin;
|
||||
let mut to = end;
|
||||
let mut resume_key = None;
|
||||
|
||||
loop {
|
||||
let mut last_key = None;
|
||||
let mut timed_out = false;
|
||||
|
||||
{
|
||||
let mut rows = conn
|
||||
.exec_stream::<Row, _, _>(&s, (from.clone(), to.clone()))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
|
||||
loop {
|
||||
match rows.try_next().await {
|
||||
Ok(Some(mut row)) => {
|
||||
let value = if params.values {
|
||||
row.take_opt::<Vec<u8>, _>(1)
|
||||
.unwrap_or_else(|| Ok(vec![]))
|
||||
.map_err(into_error)?
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let key = row
|
||||
.take_opt::<Vec<u8>, _>(0)
|
||||
.unwrap_or_else(|| Ok(vec![]))
|
||||
.map_err(into_error)?;
|
||||
|
||||
if resume_key.take().is_some_and(|resumed| resumed == key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !cb(&key, &value)? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
last_key = Some(key);
|
||||
}
|
||||
Ok(None) => break,
|
||||
Err(err) => {
|
||||
if params.first || last_key.is_none() || !is_timeout_error(&err) {
|
||||
return Err(into_error(err));
|
||||
}
|
||||
timed_out = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match last_key {
|
||||
Some(last_key) if timed_out => {
|
||||
if params.ascending {
|
||||
from.clone_from(&last_key);
|
||||
} else {
|
||||
to.clone_from(&last_key);
|
||||
}
|
||||
resume_key = Some(last_key);
|
||||
}
|
||||
_ => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_counter(
|
||||
&self,
|
||||
key: impl Into<ValueKey<ValueClass>> + Sync + Send,
|
||||
) -> trc::Result<i64> {
|
||||
let key = key.into();
|
||||
let table = char::from(key.subspace());
|
||||
let key = key.serialize(0);
|
||||
let mut conn = self.conn_pool.get_conn().await.map_err(into_error)?;
|
||||
let s = conn
|
||||
.prep(format!("SELECT v FROM {table} WHERE k = ?"))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
match conn.exec_first::<i64, _, _>(&s, (key,)).await {
|
||||
Ok(Some(num)) => Ok(num),
|
||||
Ok(None) => Ok(0),
|
||||
Err(e) => Err(into_error(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
MAX_TOKEN_LENGTH,
|
||||
mysql::{
|
||||
DELETE_CHUNK_SIZE, MIN_DELETE_CHUNK_SIZE, MysqlSearchField, MysqlStore, into_error,
|
||||
is_timeout_error,
|
||||
},
|
||||
},
|
||||
search::{
|
||||
IndexDocument, SearchComparator, SearchDocumentId, SearchFilter, SearchOperator,
|
||||
SearchQuery, SearchValue,
|
||||
},
|
||||
write::SearchIndex,
|
||||
};
|
||||
use mysql_async::{IsolationLevel, TxOpts, Value, prelude::Queryable};
|
||||
use nlp::tokenizers::word::WordTokenizer;
|
||||
use std::fmt::Write;
|
||||
|
||||
impl MysqlStore {
|
||||
pub async fn index(&self, documents: Vec<IndexDocument>) -> trc::Result<()> {
|
||||
let mut conn = self.conn_pool.get_conn().await.map_err(into_error)?;
|
||||
let mut tx_opts = TxOpts::default();
|
||||
tx_opts
|
||||
.with_consistent_snapshot(false)
|
||||
.with_isolation_level(IsolationLevel::ReadCommitted);
|
||||
let mut trx = conn.start_transaction(tx_opts).await.map_err(into_error)?;
|
||||
|
||||
for document in documents {
|
||||
let index = document.index;
|
||||
let primary_keys = index.primary_keys();
|
||||
let all_fields = index.all_fields();
|
||||
let mut fields = document.fields;
|
||||
let mut values = Vec::with_capacity(fields.len() + 2);
|
||||
let mut query = format!("INSERT INTO {} (", index.mysql_table());
|
||||
|
||||
for (i, field) in primary_keys.iter().chain(all_fields).enumerate() {
|
||||
if i > 0 {
|
||||
query.push(',');
|
||||
}
|
||||
query.push_str(field.column());
|
||||
}
|
||||
|
||||
query.push_str(") VALUES (");
|
||||
|
||||
for (i, field) in primary_keys.iter().chain(all_fields).enumerate() {
|
||||
if i > 0 {
|
||||
query.push(',');
|
||||
}
|
||||
|
||||
if let Some(value) = fields.remove(field) {
|
||||
query.push('?');
|
||||
values.push(value);
|
||||
} else {
|
||||
query.push_str("NULL");
|
||||
}
|
||||
}
|
||||
|
||||
query.push_str(") ON DUPLICATE KEY UPDATE ");
|
||||
for (i, field) in all_fields.iter().enumerate() {
|
||||
if i > 0 {
|
||||
query.push(',');
|
||||
}
|
||||
let column = field.column();
|
||||
let _ = write!(&mut query, "{column} = VALUES({column})");
|
||||
}
|
||||
|
||||
let s = trx.prep(&query).await.map_err(into_error)?;
|
||||
|
||||
trx.exec_drop(&s, values).await.map_err(into_error)?;
|
||||
}
|
||||
|
||||
trx.commit().await.map_err(into_error)
|
||||
}
|
||||
|
||||
pub async fn query<R: SearchDocumentId>(
|
||||
&self,
|
||||
index: SearchIndex,
|
||||
filters: &[SearchFilter],
|
||||
sort: &[SearchComparator],
|
||||
) -> trc::Result<Vec<R>> {
|
||||
let mut query = format!(
|
||||
"SELECT {} FROM {}",
|
||||
R::field().column(),
|
||||
index.mysql_table()
|
||||
);
|
||||
let params = build_filter(&mut query, filters);
|
||||
if !sort.is_empty() {
|
||||
build_sort(&mut query, sort);
|
||||
}
|
||||
|
||||
let mut conn = self.conn_pool.get_conn().await.map_err(into_error)?;
|
||||
let s = conn.prep(query).await.map_err(into_error)?;
|
||||
|
||||
conn.exec::<i64, _, _>(s, params)
|
||||
.await
|
||||
.map(|r| r.into_iter().map(|r| R::from_u64(r as u64)).collect())
|
||||
.map_err(into_error)
|
||||
}
|
||||
|
||||
pub async fn unindex(&self, filter: SearchQuery) -> trc::Result<u64> {
|
||||
let table = filter.index.mysql_table();
|
||||
let mut query = format!("DELETE FROM {table} ");
|
||||
let params = build_filter(&mut query, &filter.filters);
|
||||
|
||||
let mut conn = self.conn_pool.get_conn().await.map_err(into_error)?;
|
||||
let s = conn.prep(&query).await.map_err(into_error)?;
|
||||
|
||||
match conn.exec_drop(s, params.clone()).await {
|
||||
Ok(_) => return Ok(conn.affected_rows()),
|
||||
Err(err) if is_timeout_error(&err) => (),
|
||||
Err(err) => return Err(into_error(err)),
|
||||
}
|
||||
|
||||
let mut chunk_size = DELETE_CHUNK_SIZE;
|
||||
let mut deleted = 0;
|
||||
|
||||
loop {
|
||||
let s = conn
|
||||
.prep(format!("{query} LIMIT {chunk_size}"))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
|
||||
loop {
|
||||
match conn.exec_drop(&s, params.clone()).await {
|
||||
Ok(_) => {
|
||||
let affected = conn.affected_rows();
|
||||
if affected == 0 {
|
||||
return Ok(deleted);
|
||||
}
|
||||
deleted += affected;
|
||||
}
|
||||
Err(err) if is_timeout_error(&err) && chunk_size > MIN_DELETE_CHUNK_SIZE => {
|
||||
chunk_size = (chunk_size / 2).max(MIN_DELETE_CHUNK_SIZE);
|
||||
break;
|
||||
}
|
||||
Err(err) => return Err(into_error(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_filter(query: &mut String, filters: &[SearchFilter]) -> Vec<Value> {
|
||||
if filters.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
query.push_str(" WHERE ");
|
||||
let mut operator_stack = Vec::new();
|
||||
let mut operator = &SearchFilter::And;
|
||||
let mut is_first = true;
|
||||
let mut values: Vec<Value> = Vec::new();
|
||||
|
||||
for filter in filters {
|
||||
match filter {
|
||||
SearchFilter::Operator { field, op, value } => {
|
||||
if !is_first {
|
||||
match operator {
|
||||
SearchFilter::And => query.push_str(" AND "),
|
||||
SearchFilter::Or => query.push_str(" OR "),
|
||||
_ => (),
|
||||
}
|
||||
} else {
|
||||
is_first = false;
|
||||
}
|
||||
|
||||
if field.is_text() && matches!(op, SearchOperator::Equal | SearchOperator::Contains)
|
||||
{
|
||||
let (value, mode) = match (value, op) {
|
||||
(SearchValue::Text { value, .. }, SearchOperator::Equal) => {
|
||||
(Value::Bytes(format!("{value:?}").into_bytes()), "BOOLEAN")
|
||||
}
|
||||
(SearchValue::Text { value, .. }, ..) => {
|
||||
let mut text_query = String::with_capacity(value.len() + 1);
|
||||
|
||||
for item in WordTokenizer::new(value, MAX_TOKEN_LENGTH) {
|
||||
if !text_query.is_empty() {
|
||||
text_query.push(' ');
|
||||
}
|
||||
text_query.push('+');
|
||||
text_query.push_str(&item.word);
|
||||
}
|
||||
|
||||
(Value::Bytes(text_query.into_bytes()), "BOOLEAN")
|
||||
}
|
||||
_ => {
|
||||
debug_assert!(false, "Invalid search value for text field");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let _ = write!(query, "MATCH({}) AGAINST(? IN {mode} MODE)", field.column());
|
||||
values.push(value);
|
||||
} else if let SearchValue::KeyValues(kv) = value {
|
||||
let (key, value) = kv.iter().next().unwrap();
|
||||
|
||||
values.push(Value::Bytes(format!("$.{key:?}").into_bytes()));
|
||||
|
||||
if !value.is_empty() {
|
||||
if op == &SearchOperator::Equal {
|
||||
let _ = write!(query, "JSON_EXTRACT({}, ?) = ?", field.column());
|
||||
values.push(Value::Bytes(value.as_bytes().to_vec()));
|
||||
} else {
|
||||
let _ = write!(query, "JSON_EXTRACT({}, ?) LIKE ?", field.column(),);
|
||||
values.push(Value::Bytes(format!("%{value}%").into_bytes()));
|
||||
}
|
||||
} else {
|
||||
let _ = write!(query, "JSON_CONTAINS_PATH({}, 'one', ?)", field.column(),);
|
||||
}
|
||||
} else {
|
||||
query.push_str(field.column());
|
||||
query.push(' ');
|
||||
op.write_mysql(query);
|
||||
values.push(to_mysql(value));
|
||||
}
|
||||
}
|
||||
SearchFilter::And | SearchFilter::Or => {
|
||||
if !is_first {
|
||||
match operator {
|
||||
SearchFilter::And => query.push_str(" AND "),
|
||||
SearchFilter::Or => query.push_str(" OR "),
|
||||
_ => (),
|
||||
}
|
||||
} else {
|
||||
is_first = false;
|
||||
}
|
||||
|
||||
operator_stack.push((operator, is_first));
|
||||
operator = filter;
|
||||
is_first = true;
|
||||
query.push('(');
|
||||
}
|
||||
SearchFilter::Not => {
|
||||
if !is_first {
|
||||
match operator {
|
||||
SearchFilter::And => query.push_str(" AND "),
|
||||
SearchFilter::Or => query.push_str(" OR "),
|
||||
_ => (),
|
||||
}
|
||||
} else {
|
||||
is_first = false;
|
||||
}
|
||||
|
||||
operator_stack.push((operator, is_first));
|
||||
operator = &SearchFilter::And;
|
||||
is_first = true;
|
||||
query.push_str("NOT (");
|
||||
}
|
||||
SearchFilter::End => {
|
||||
let p = operator_stack.pop().unwrap_or((&SearchFilter::And, true));
|
||||
operator = p.0;
|
||||
is_first = p.1;
|
||||
query.push(')');
|
||||
}
|
||||
SearchFilter::DocumentSet(_) => {
|
||||
debug_assert!(
|
||||
false,
|
||||
"DocumentSet filters are not supported in Postgres backend"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
values
|
||||
}
|
||||
|
||||
fn build_sort(query: &mut String, sort: &[SearchComparator]) {
|
||||
query.push_str(" ORDER BY ");
|
||||
for (i, comparator) in sort.iter().enumerate() {
|
||||
if i > 0 {
|
||||
query.push_str(", ");
|
||||
}
|
||||
match comparator {
|
||||
SearchComparator::Field { field, ascending } => {
|
||||
query.push_str(field.column());
|
||||
if *ascending {
|
||||
query.push_str(" ASC");
|
||||
} else {
|
||||
query.push_str(" DESC");
|
||||
}
|
||||
}
|
||||
SearchComparator::DocumentSet { .. } | SearchComparator::SortedSet { .. } => {
|
||||
debug_assert!(
|
||||
false,
|
||||
"DocumentSet and SortedSet comparators are not supported "
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchOperator {
|
||||
fn write_mysql(&self, query: &mut String) {
|
||||
match self {
|
||||
SearchOperator::LowerThan => {
|
||||
let _ = write!(query, "< ?");
|
||||
}
|
||||
SearchOperator::LowerEqualThan => {
|
||||
let _ = write!(query, "<= ?");
|
||||
}
|
||||
SearchOperator::GreaterThan => {
|
||||
let _ = write!(query, "> ?");
|
||||
}
|
||||
SearchOperator::GreaterEqualThan => {
|
||||
let _ = write!(query, ">= ?");
|
||||
}
|
||||
SearchOperator::Equal => {
|
||||
let _ = write!(query, "= ?");
|
||||
}
|
||||
SearchOperator::Contains => {
|
||||
let _ = write!(query, "LIKE '%' CONCAT('%', ?, '%')");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SearchValue> for Value {
|
||||
fn from(value: SearchValue) -> Self {
|
||||
match value {
|
||||
SearchValue::Text { mut value, .. } => {
|
||||
// Truncate values larger than 16MB to avoid MySQL errors
|
||||
if value.len() > 16_777_214 {
|
||||
let pos = value.floor_char_boundary(16_777_214);
|
||||
value.truncate(pos);
|
||||
}
|
||||
|
||||
Value::Bytes(value.into_bytes())
|
||||
}
|
||||
SearchValue::KeyValues(vec_map) => serde_json::to_string(&vec_map)
|
||||
.map(|v| Value::Bytes(v.into_bytes()))
|
||||
.unwrap_or(Value::NULL),
|
||||
SearchValue::Int(i) => Value::Int(i),
|
||||
SearchValue::Uint(i) => Value::Int(i as i64),
|
||||
SearchValue::Boolean(b) => Value::Int(b as i64),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_mysql(value: &SearchValue) -> Value {
|
||||
match value {
|
||||
SearchValue::Text { value, .. } => Value::Bytes(value.as_bytes().to_vec()),
|
||||
SearchValue::KeyValues(vec_map) => serde_json::to_string(&vec_map)
|
||||
.map(|v| Value::Bytes(v.into_bytes()))
|
||||
.unwrap_or(Value::NULL),
|
||||
SearchValue::Int(i) => Value::Int(*i),
|
||||
SearchValue::Uint(i) => Value::Int(*i as i64),
|
||||
SearchValue::Boolean(b) => Value::Int(*b as i64),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{DELETE_CHUNK_SIZE, MIN_DELETE_CHUNK_SIZE, MysqlStore, into_error, is_timeout_error};
|
||||
use crate::{
|
||||
IndexKey, Key, LogKey, SUBSPACE_COUNTER, SUBSPACE_IN_MEMORY_COUNTER, SUBSPACE_QUOTA,
|
||||
SUBSPACE_REGISTRY_IDX,
|
||||
write::{
|
||||
AssignedIds, Batch, MAX_COMMIT_ATTEMPTS, MAX_COMMIT_TIME, MergeResult, Operation,
|
||||
ValueClass, ValueOp,
|
||||
},
|
||||
};
|
||||
use ahash::AHashMap;
|
||||
use mysql_async::{Conn, Error, IsolationLevel, TxOpts, params, prelude::Queryable};
|
||||
use rand::RngExt;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum CommitError {
|
||||
Mysql(mysql_async::Error),
|
||||
Internal(trc::Error),
|
||||
//Retry,
|
||||
}
|
||||
|
||||
impl MysqlStore {
|
||||
pub(crate) async fn write(&self, mut batch: Batch<'_>) -> trc::Result<AssignedIds> {
|
||||
let start = Instant::now();
|
||||
let mut retry_count = 0;
|
||||
let mut conn = self.conn_pool.get_conn().await.map_err(into_error)?;
|
||||
|
||||
loop {
|
||||
let err = match self.write_trx(&mut conn, &mut batch).await {
|
||||
Ok(result) => {
|
||||
return Ok(result);
|
||||
}
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
let _ = conn.query_drop("ROLLBACK;").await;
|
||||
|
||||
match err {
|
||||
CommitError::Mysql(Error::Server(err))
|
||||
if [1062, 1213].contains(&err.code)
|
||||
&& retry_count < MAX_COMMIT_ATTEMPTS
|
||||
&& start.elapsed() < MAX_COMMIT_TIME => {}
|
||||
/*CommitError::Retry => {
|
||||
if retry_count > MAX_COMMIT_ATTEMPTS || start.elapsed() > MAX_COMMIT_TIME {
|
||||
return Err(trc::StoreEvent::AssertValueFailed
|
||||
.into_err()
|
||||
.caused_by(trc::location!()));
|
||||
}
|
||||
}*/
|
||||
CommitError::Mysql(err) => {
|
||||
return Err(into_error(err));
|
||||
}
|
||||
CommitError::Internal(err) => {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
|
||||
let backoff = rand::rng().random_range(50..=300);
|
||||
tokio::time::sleep(Duration::from_millis(backoff)).await;
|
||||
retry_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_trx(
|
||||
&self,
|
||||
conn: &mut Conn,
|
||||
batch: &mut Batch<'_>,
|
||||
) -> Result<AssignedIds, CommitError> {
|
||||
let has_changes = !batch.changes.is_empty();
|
||||
let mut account_id = u32::MAX;
|
||||
let mut collection = u8::MAX;
|
||||
let mut document_id = u32::MAX;
|
||||
let mut change_id = 0u64;
|
||||
let mut asserted_values = AHashMap::new();
|
||||
let mut tx_opts = TxOpts::default();
|
||||
tx_opts
|
||||
.with_consistent_snapshot(false)
|
||||
.with_isolation_level(IsolationLevel::ReadCommitted);
|
||||
let mut trx = conn.start_transaction(tx_opts).await?;
|
||||
let mut result = AssignedIds::default();
|
||||
|
||||
if has_changes {
|
||||
for &account_id in batch.changes.keys() {
|
||||
let key = ValueClass::ChangeId.serialize(account_id, 0, 0, 0);
|
||||
let s = trx
|
||||
.prep(concat!(
|
||||
"INSERT INTO n (k, v) VALUES (:k, LAST_INSERT_ID(1)) ",
|
||||
"ON DUPLICATE KEY UPDATE v = LAST_INSERT_ID(v + 1)"
|
||||
))
|
||||
.await?;
|
||||
trx.exec_drop(&s, params! {"k" => key}).await?;
|
||||
let s = trx.prep("SELECT LAST_INSERT_ID()").await?;
|
||||
let change_id = trx.exec_first::<i64, _, _>(&s, ()).await?.ok_or_else(|| {
|
||||
mysql_async::Error::Io(mysql_async::IoError::Io(std::io::Error::other(
|
||||
"LAST_INSERT_ID() did not return a value",
|
||||
)))
|
||||
})?;
|
||||
result.push_change_id(account_id, change_id as u64);
|
||||
}
|
||||
}
|
||||
|
||||
for op in batch.ops.iter_mut() {
|
||||
match op {
|
||||
Operation::AccountId {
|
||||
account_id: account_id_,
|
||||
} => {
|
||||
account_id = *account_id_;
|
||||
if has_changes {
|
||||
change_id = result.set_current_change_id(account_id)?;
|
||||
}
|
||||
}
|
||||
Operation::Collection {
|
||||
collection: collection_,
|
||||
} => {
|
||||
collection = u8::from(*collection_);
|
||||
}
|
||||
Operation::DocumentId {
|
||||
document_id: document_id_,
|
||||
} => {
|
||||
document_id = *document_id_;
|
||||
}
|
||||
Operation::Value { class, op } => {
|
||||
let key = class.serialize(account_id, collection, document_id, 0);
|
||||
let subspace = class.subspace(collection);
|
||||
let table = char::from(subspace);
|
||||
|
||||
match op {
|
||||
ValueOp::Set(value) => {
|
||||
if subspace != SUBSPACE_REGISTRY_IDX {
|
||||
let exists = asserted_values.get(&key);
|
||||
let s = if let Some(exists) = exists {
|
||||
if *exists {
|
||||
trx.prep(format!(
|
||||
"UPDATE {} SET v = :v WHERE k = :k",
|
||||
table
|
||||
))
|
||||
.await?
|
||||
} else {
|
||||
trx.prep(format!(
|
||||
"INSERT INTO {} (k, v) VALUES (:k, :v)",
|
||||
table
|
||||
))
|
||||
.await?
|
||||
}
|
||||
} else {
|
||||
trx
|
||||
.prep(
|
||||
format!("INSERT INTO {} (k, v) VALUES (:k, :v) ON DUPLICATE KEY UPDATE v = VALUES(v)", table),
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
match trx
|
||||
.exec_drop(&s, params! {"k" => key, "v" => &*value})
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
if trx.affected_rows() == 0 {
|
||||
trx.rollback().await?;
|
||||
return Err(trc::StoreEvent::AssertValueFailed
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.into());
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
trx.rollback().await?;
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let s = trx.prep("INSERT IGNORE INTO b (k) VALUES (?)").await?;
|
||||
trx.exec_drop(&s, (key,)).await?;
|
||||
}
|
||||
}
|
||||
ValueOp::SetFnc(set_op) => {
|
||||
let value = (set_op.fnc)(&set_op.params, &result)?;
|
||||
let exists = asserted_values.get(&key);
|
||||
let s = if let Some(exists) = exists {
|
||||
if *exists {
|
||||
trx.prep(format!("UPDATE {} SET v = :v WHERE k = :k", table))
|
||||
.await?
|
||||
} else {
|
||||
trx.prep(format!(
|
||||
"INSERT INTO {} (k, v) VALUES (:k, :v)",
|
||||
table
|
||||
))
|
||||
.await?
|
||||
}
|
||||
} else {
|
||||
trx
|
||||
.prep(
|
||||
format!("INSERT INTO {} (k, v) VALUES (:k, :v) ON DUPLICATE KEY UPDATE v = VALUES(v)", table),
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
match trx.exec_drop(&s, params! {"k" => key, "v" => &value}).await {
|
||||
Ok(_) => {
|
||||
if trx.affected_rows() == 0 {
|
||||
trx.rollback().await?;
|
||||
return Err(trc::StoreEvent::AssertValueFailed
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.into());
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
trx.rollback().await?;
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
ValueOp::MergeFnc(merge_op) => {
|
||||
let s = trx
|
||||
.prep(format!("SELECT v FROM {} WHERE k = ? FOR UPDATE", table))
|
||||
.await?;
|
||||
let (exists, merge_result) = trx
|
||||
.exec_first::<Vec<u8>, _, _>(&s, (&key,))
|
||||
.await?
|
||||
.map(|bytes| {
|
||||
(merge_op.fnc)(&merge_op.params, &result, Some(bytes.as_ref()))
|
||||
.map(|v| (true, v))
|
||||
.map_err(CommitError::from)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
(merge_op.fnc)(&merge_op.params, &result, None)
|
||||
.map(|v| (false, v))
|
||||
.map_err(CommitError::from)
|
||||
})?;
|
||||
|
||||
let s = if exists {
|
||||
trx.prep(format!("UPDATE {} SET v = :v WHERE k = :k", table))
|
||||
.await?
|
||||
} else {
|
||||
trx.prep(format!("INSERT INTO {} (k, v) VALUES (:k, :v)", table))
|
||||
.await?
|
||||
};
|
||||
|
||||
match merge_result {
|
||||
MergeResult::Update(value) => {
|
||||
if let Err(err) =
|
||||
trx.exec_drop(&s, params! {"k" => key, "v" => &value}).await
|
||||
{
|
||||
trx.rollback().await?;
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
MergeResult::Delete if exists => {
|
||||
// Update asserted value
|
||||
if let Some(exists) = asserted_values.get_mut(&key) {
|
||||
*exists = false;
|
||||
}
|
||||
|
||||
let s = trx
|
||||
.prep(format!("DELETE FROM {} WHERE k = ?", table))
|
||||
.await?;
|
||||
trx.exec_drop(&s, (key,)).await?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
ValueOp::AtomicAdd(by) => {
|
||||
if *by >= 0 {
|
||||
let s = trx
|
||||
.prep(format!(
|
||||
concat!(
|
||||
"INSERT INTO {} (k, v) VALUES (?, ?) ",
|
||||
"ON DUPLICATE KEY UPDATE v = v + VALUES(v)"
|
||||
),
|
||||
table
|
||||
))
|
||||
.await?;
|
||||
trx.exec_drop(&s, (key, &*by)).await?;
|
||||
} else {
|
||||
let s = trx
|
||||
.prep(format!("UPDATE {table} SET v = v + ? WHERE k = ?"))
|
||||
.await?;
|
||||
trx.exec_drop(&s, (&*by, key)).await?;
|
||||
}
|
||||
}
|
||||
ValueOp::AddAndGet(by) => {
|
||||
let s = trx
|
||||
.prep(format!(
|
||||
concat!(
|
||||
"INSERT INTO {} (k, v) VALUES (:k, LAST_INSERT_ID(:v)) ",
|
||||
"ON DUPLICATE KEY UPDATE v = LAST_INSERT_ID(v + :v)"
|
||||
),
|
||||
table
|
||||
))
|
||||
.await?;
|
||||
trx.exec_drop(&s, params! {"k" => key, "v" => &*by}).await?;
|
||||
let s = trx.prep("SELECT LAST_INSERT_ID()").await?;
|
||||
result.push_counter_id(
|
||||
trx.exec_first::<i64, _, _>(&s, ()).await?.ok_or_else(|| {
|
||||
mysql_async::Error::Io(mysql_async::IoError::Io(
|
||||
std::io::Error::other(
|
||||
"LAST_INSERT_ID() did not return a value",
|
||||
),
|
||||
))
|
||||
})?,
|
||||
);
|
||||
}
|
||||
ValueOp::Clear => {
|
||||
// Update asserted value
|
||||
if let Some(exists) = asserted_values.get_mut(&key) {
|
||||
*exists = false;
|
||||
}
|
||||
|
||||
let s = trx
|
||||
.prep(format!("DELETE FROM {} WHERE k = ?", table))
|
||||
.await?;
|
||||
trx.exec_drop(&s, (key,)).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Operation::Index { field, key, set } => {
|
||||
let key = IndexKey {
|
||||
account_id,
|
||||
collection,
|
||||
document_id,
|
||||
field: *field,
|
||||
key: &*key,
|
||||
}
|
||||
.serialize(0);
|
||||
|
||||
let s = if *set {
|
||||
trx.prep("INSERT IGNORE INTO i (k) VALUES (?)").await?
|
||||
} else {
|
||||
trx.prep("DELETE FROM i WHERE k = ?").await?
|
||||
};
|
||||
trx.exec_drop(&s, (key,)).await?;
|
||||
}
|
||||
Operation::Log { collection, set } => {
|
||||
let key = LogKey {
|
||||
account_id,
|
||||
collection: u8::from(*collection),
|
||||
change_id,
|
||||
}
|
||||
.serialize(0);
|
||||
|
||||
let s = trx
|
||||
.prep("INSERT INTO l (k, v) VALUES (?, ?) ON DUPLICATE KEY UPDATE v = VALUES(v)")
|
||||
.await?;
|
||||
|
||||
trx.exec_drop(&s, (key, &*set)).await?;
|
||||
}
|
||||
Operation::AssertValue {
|
||||
class,
|
||||
assert_value,
|
||||
} => {
|
||||
let key = class.serialize(account_id, collection, document_id, 0);
|
||||
let table = char::from(class.subspace(collection));
|
||||
|
||||
let s = trx
|
||||
.prep(format!("SELECT v FROM {} WHERE k = ? FOR UPDATE", table))
|
||||
.await?;
|
||||
let (exists, matches) = trx
|
||||
.exec_first::<Vec<u8>, _, _>(&s, (&key,))
|
||||
.await?
|
||||
.map(|bytes| (true, assert_value.matches(&bytes)))
|
||||
.unwrap_or_else(|| (false, assert_value.is_none()));
|
||||
if !matches {
|
||||
trx.rollback().await?;
|
||||
return Err(trc::StoreEvent::AssertValueFailed
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.into());
|
||||
}
|
||||
asserted_values.insert(key, exists);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trx.commit().await.map(|_| result).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) async fn purge_store(&self) -> trc::Result<()> {
|
||||
let mut conn = self.conn_pool.get_conn().await.map_err(into_error)?;
|
||||
for subspace in [SUBSPACE_QUOTA, SUBSPACE_COUNTER, SUBSPACE_IN_MEMORY_COUNTER] {
|
||||
purge_table(&mut conn, char::from(subspace)).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_range(&self, from: impl Key, to: impl Key) -> trc::Result<()> {
|
||||
let mut conn = self.conn_pool.get_conn().await.map_err(into_error)?;
|
||||
let table = char::from(from.subspace());
|
||||
let mut from = from.serialize(0);
|
||||
let to = to.serialize(0);
|
||||
|
||||
let delete = conn
|
||||
.prep(format!("DELETE FROM {table} WHERE k >= ? AND k < ?"))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
|
||||
match conn.exec_drop(&delete, (&from, &to)).await {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(err) if is_timeout_error(&err) => (),
|
||||
Err(err) => return Err(into_error(err)),
|
||||
}
|
||||
|
||||
let mut chunk_size = DELETE_CHUNK_SIZE;
|
||||
|
||||
loop {
|
||||
let boundary = conn
|
||||
.prep(format!(
|
||||
"SELECT k FROM {table} WHERE k >= ? AND k < ? ORDER BY k ASC LIMIT 1 OFFSET {chunk_size}"
|
||||
))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
|
||||
loop {
|
||||
let next = match conn
|
||||
.exec_first::<Vec<u8>, _, _>(&boundary, (&from, &to))
|
||||
.await
|
||||
{
|
||||
Ok(next) => next,
|
||||
Err(err) if is_timeout_error(&err) && chunk_size > MIN_DELETE_CHUNK_SIZE => {
|
||||
chunk_size = (chunk_size / 2).max(MIN_DELETE_CHUNK_SIZE);
|
||||
break;
|
||||
}
|
||||
Err(err) => return Err(into_error(err)),
|
||||
};
|
||||
|
||||
match conn
|
||||
.exec_drop(&delete, (&from, next.as_ref().unwrap_or(&to)))
|
||||
.await
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(err) if is_timeout_error(&err) && chunk_size > MIN_DELETE_CHUNK_SIZE => {
|
||||
chunk_size = (chunk_size / 2).max(MIN_DELETE_CHUNK_SIZE);
|
||||
break;
|
||||
}
|
||||
Err(err) => return Err(into_error(err)),
|
||||
}
|
||||
|
||||
match next {
|
||||
Some(next) => from = next,
|
||||
None => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn purge_table(conn: &mut Conn, table: char) -> trc::Result<()> {
|
||||
let s = conn
|
||||
.prep(format!("DELETE FROM {table} WHERE v = 0"))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
|
||||
match conn.exec_drop(&s, ()).await {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(err) if is_timeout_error(&err) => (),
|
||||
Err(err) => return Err(into_error(err)),
|
||||
}
|
||||
|
||||
let purge = conn
|
||||
.prep(format!(
|
||||
"DELETE FROM {table} WHERE v = 0 AND k >= ? AND k < ?"
|
||||
))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
let purge_last = conn
|
||||
.prep(format!("DELETE FROM {table} WHERE v = 0 AND k >= ?"))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
let mut chunk_size = DELETE_CHUNK_SIZE;
|
||||
let mut from = Vec::new();
|
||||
|
||||
loop {
|
||||
let boundary = conn
|
||||
.prep(format!(
|
||||
"SELECT k FROM {table} WHERE k >= ? ORDER BY k ASC LIMIT 1 OFFSET {chunk_size}"
|
||||
))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
|
||||
loop {
|
||||
let next = match conn.exec_first::<Vec<u8>, _, _>(&boundary, (&from,)).await {
|
||||
Ok(next) => next,
|
||||
Err(err) if is_timeout_error(&err) && chunk_size > MIN_DELETE_CHUNK_SIZE => {
|
||||
chunk_size = (chunk_size / 2).max(MIN_DELETE_CHUNK_SIZE);
|
||||
break;
|
||||
}
|
||||
Err(err) => return Err(into_error(err)),
|
||||
};
|
||||
|
||||
let result = match &next {
|
||||
Some(next) => conn.exec_drop(&purge, (&from, next)).await,
|
||||
None => conn.exec_drop(&purge_last, (&from,)).await,
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(_) => (),
|
||||
Err(err) if is_timeout_error(&err) && chunk_size > MIN_DELETE_CHUNK_SIZE => {
|
||||
chunk_size = (chunk_size / 2).max(MIN_DELETE_CHUNK_SIZE);
|
||||
break;
|
||||
}
|
||||
Err(err) => return Err(into_error(err)),
|
||||
}
|
||||
|
||||
match next {
|
||||
Some(next) => from = next,
|
||||
None => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<trc::Error> for CommitError {
|
||||
fn from(err: trc::Error) -> Self {
|
||||
CommitError::Internal(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mysql_async::Error> for CommitError {
|
||||
fn from(err: mysql_async::Error) -> Self {
|
||||
CommitError::Mysql(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::backend::postgres::into_pool_error;
|
||||
|
||||
use super::{PostgresStore, into_error};
|
||||
|
||||
impl PostgresStore {
|
||||
pub(crate) async fn get_blob(
|
||||
&self,
|
||||
key: &[u8],
|
||||
range: Range<usize>,
|
||||
) -> trc::Result<Option<Vec<u8>>> {
|
||||
let conn = self.conn_pool.get().await.map_err(into_pool_error)?;
|
||||
let s = conn
|
||||
.prepare_cached("SELECT v FROM t WHERE k = $1")
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
conn.query_opt(&s, &[&key])
|
||||
.await
|
||||
.and_then(|row| {
|
||||
if let Some(row) = row {
|
||||
Ok(Some(if range.start == 0 && range.end == usize::MAX {
|
||||
row.try_get::<_, Vec<u8>>(0)?
|
||||
} else {
|
||||
let bytes = row.try_get::<_, &[u8]>(0)?;
|
||||
bytes
|
||||
.get(range.start..std::cmp::min(bytes.len(), range.end))
|
||||
.unwrap_or_default()
|
||||
.to_vec()
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
.map_err(into_error)
|
||||
}
|
||||
|
||||
pub(crate) async fn put_blob(&self, key: &[u8], data: &[u8]) -> trc::Result<()> {
|
||||
let conn = self.conn_pool.get().await.map_err(into_pool_error)?;
|
||||
let s = conn
|
||||
.prepare_cached(
|
||||
"INSERT INTO t (k, v) VALUES ($1, $2) ON CONFLICT (k) DO UPDATE SET v = EXCLUDED.v",
|
||||
)
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
conn.execute(&s, &[&key, &data])
|
||||
.await
|
||||
.map_err(into_error)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_blob(&self, key: &[u8]) -> trc::Result<bool> {
|
||||
let conn = self.conn_pool.get().await.map_err(into_pool_error)?;
|
||||
let s = conn
|
||||
.prepare_cached("DELETE FROM t WHERE k = $1")
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
conn.execute(&s, &[&key])
|
||||
.await
|
||||
.map_err(into_error)
|
||||
.map(|hits| hits > 0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{QueryResult, QueryType, backend::postgres::into_pool_error};
|
||||
|
||||
use bytes::BytesMut;
|
||||
use futures::{TryStreamExt, pin_mut};
|
||||
use tokio_postgres::types::{FromSql, ToSql, Type};
|
||||
|
||||
use crate::IntoRows;
|
||||
|
||||
use super::{PostgresStore, into_error};
|
||||
|
||||
impl PostgresStore {
|
||||
pub(crate) async fn sql_query<T: QueryResult>(
|
||||
&self,
|
||||
query: &str,
|
||||
params_: &[crate::Value<'_>],
|
||||
) -> trc::Result<T> {
|
||||
let conn = self.conn_pool.get().await.map_err(into_pool_error)?;
|
||||
let s = conn.prepare_cached(query).await.map_err(into_error)?;
|
||||
let params = params_
|
||||
.iter()
|
||||
.map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
match T::query_type() {
|
||||
QueryType::Execute => conn
|
||||
.execute(&s, params.as_slice())
|
||||
.await
|
||||
.map_or_else(|e| Err(into_error(e)), |r| Ok(T::from_exec(r as usize))),
|
||||
QueryType::Exists => {
|
||||
let rows = conn.query_raw(&s, params).await.map_err(into_error)?;
|
||||
pin_mut!(rows);
|
||||
rows.try_next()
|
||||
.await
|
||||
.map_or_else(|e| Err(into_error(e)), |r| Ok(T::from_exists(r.is_some())))
|
||||
}
|
||||
QueryType::QueryOne => conn
|
||||
.query_opt(&s, params.as_slice())
|
||||
.await
|
||||
.map_or_else(|e| Err(into_error(e)), |r| Ok(T::from_query_one(r))),
|
||||
QueryType::QueryAll => conn
|
||||
.query(&s, params.as_slice())
|
||||
.await
|
||||
.map_or_else(|e| Err(into_error(e)), |r| Ok(T::from_query_all(r))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql for crate::Value<'_> {
|
||||
fn to_sql(
|
||||
&self,
|
||||
ty: &tokio_postgres::types::Type,
|
||||
out: &mut BytesMut,
|
||||
) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match self {
|
||||
crate::Value::Integer(v) => match *ty {
|
||||
Type::CHAR => (*v as i8).to_sql(ty, out),
|
||||
Type::INT2 => (*v as i16).to_sql(ty, out),
|
||||
Type::INT4 => (*v as i32).to_sql(ty, out),
|
||||
_ => v.to_sql(ty, out),
|
||||
},
|
||||
crate::Value::Bool(v) => v.to_sql(ty, out),
|
||||
crate::Value::Float(v) => {
|
||||
if matches!(ty, &Type::FLOAT4) {
|
||||
(*v as f32).to_sql(ty, out)
|
||||
} else {
|
||||
v.to_sql(ty, out)
|
||||
}
|
||||
}
|
||||
crate::Value::Text(v) => v.to_sql(ty, out),
|
||||
crate::Value::Blob(v) => v.to_sql(ty, out),
|
||||
crate::Value::Null => None::<String>.to_sql(ty, out),
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts(_: &tokio_postgres::types::Type) -> bool
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
fn to_sql_checked(
|
||||
&self,
|
||||
ty: &tokio_postgres::types::Type,
|
||||
out: &mut BytesMut,
|
||||
) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
|
||||
match self {
|
||||
crate::Value::Integer(v) => match *ty {
|
||||
Type::CHAR => (*v as i8).to_sql_checked(ty, out),
|
||||
Type::INT2 => (*v as i16).to_sql_checked(ty, out),
|
||||
Type::INT4 => (*v as i32).to_sql_checked(ty, out),
|
||||
_ => v.to_sql_checked(ty, out),
|
||||
},
|
||||
crate::Value::Bool(v) => v.to_sql_checked(ty, out),
|
||||
crate::Value::Float(v) => {
|
||||
if matches!(ty, &Type::FLOAT4) {
|
||||
(*v as f32).to_sql_checked(ty, out)
|
||||
} else {
|
||||
v.to_sql_checked(ty, out)
|
||||
}
|
||||
}
|
||||
crate::Value::Text(v) => v.to_sql_checked(ty, out),
|
||||
crate::Value::Blob(v) => v.to_sql_checked(ty, out),
|
||||
crate::Value::Null => None::<String>.to_sql_checked(ty, out),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoRows for Vec<tokio_postgres::Row> {
|
||||
fn into_rows(self) -> crate::Rows {
|
||||
crate::Rows {
|
||||
rows: self
|
||||
.into_iter()
|
||||
.map(|r| crate::Row {
|
||||
values: (0..r.len())
|
||||
.map(|idx| r.try_get(idx).unwrap_or(crate::Value::Null))
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_named_rows(self) -> crate::NamedRows {
|
||||
crate::NamedRows {
|
||||
names: self
|
||||
.first()
|
||||
.map(|r| r.columns().iter().map(|c| c.name().to_string()).collect())
|
||||
.unwrap_or_default(),
|
||||
rows: self
|
||||
.into_iter()
|
||||
.map(|r| crate::Row {
|
||||
values: (0..r.len())
|
||||
.map(|idx| r.try_get(idx).unwrap_or(crate::Value::Null))
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_row(self) -> Option<crate::Row> {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoRows for Option<tokio_postgres::Row> {
|
||||
fn into_row(self) -> Option<crate::Row> {
|
||||
self.map(|row| crate::Row {
|
||||
values: (0..row.len())
|
||||
.map(|idx| row.try_get(idx).unwrap_or(crate::Value::Null))
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn into_rows(self) -> crate::Rows {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn into_named_rows(self) -> crate::NamedRows {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql<'_> for crate::Value<'static> {
|
||||
fn from_sql(
|
||||
ty: &tokio_postgres::types::Type,
|
||||
raw: &'_ [u8],
|
||||
) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
|
||||
match ty {
|
||||
&Type::VARCHAR | &Type::TEXT | &Type::BPCHAR | &Type::NAME | &Type::UNKNOWN => {
|
||||
String::from_sql(ty, raw).map(|s| crate::Value::Text(s.into()))
|
||||
}
|
||||
&Type::BOOL => bool::from_sql(ty, raw).map(crate::Value::Bool),
|
||||
&Type::CHAR => i8::from_sql(ty, raw).map(|v| crate::Value::Integer(v as i64)),
|
||||
&Type::INT2 => i16::from_sql(ty, raw).map(|v| crate::Value::Integer(v as i64)),
|
||||
&Type::INT4 => i32::from_sql(ty, raw).map(|v| crate::Value::Integer(v as i64)),
|
||||
&Type::INT8 | &Type::OID => i64::from_sql(ty, raw).map(crate::Value::Integer),
|
||||
&Type::FLOAT4 | &Type::FLOAT8 => f64::from_sql(ty, raw).map(crate::Value::Float),
|
||||
ty if (ty.name() == "citext"
|
||||
|| ty.name() == "ltree"
|
||||
|| ty.name() == "lquery"
|
||||
|| ty.name() == "ltxtquery") =>
|
||||
{
|
||||
String::from_sql(ty, raw).map(|s| crate::Value::Text(s.into()))
|
||||
}
|
||||
_ => Vec::<u8>::from_sql(ty, raw).map(|b| crate::Value::Blob(b.into())),
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts(_: &tokio_postgres::types::Type) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{PostgresStore, into_error};
|
||||
use crate::{
|
||||
backend::postgres::{
|
||||
PsqlSearchField, into_pool_error,
|
||||
search::{PG_FALLBACK_LANG, PG_LANGS, PG_UNSTEMMED_LANG},
|
||||
tls::MakeRustlsConnect,
|
||||
},
|
||||
search::{
|
||||
CalendarSearchField, ContactSearchField, EmailSearchField, SearchableField,
|
||||
TracingSearchField,
|
||||
},
|
||||
*,
|
||||
};
|
||||
use ::registry::schema::{enums::PostgreSqlRecyclingMethod, structs};
|
||||
use ahash::AHashSet;
|
||||
use deadpool_postgres::{
|
||||
Config, ManagerConfig, Object, Pool, PoolConfig, RecyclingMethod, Runtime,
|
||||
};
|
||||
use tokio_postgres::NoTls;
|
||||
use utils::tls::rustls_client_config;
|
||||
|
||||
impl PostgresStore {
|
||||
pub async fn open(config: structs::PostgreSqlStore) -> Result<Store, String> {
|
||||
let mut cfg = Config::new();
|
||||
cfg.dbname = config.database.into();
|
||||
cfg.host = config.host.into();
|
||||
cfg.user = config.auth_username;
|
||||
cfg.password = config.auth_secret.secret().await?.map(|v| v.into_owned());
|
||||
cfg.port = (config.port as u16).into();
|
||||
cfg.connect_timeout = config.timeout.map(|t| t.into_inner());
|
||||
cfg.options = config.options;
|
||||
cfg.manager = Some(ManagerConfig {
|
||||
recycling_method: match config.pool_recycling_method {
|
||||
PostgreSqlRecyclingMethod::Fast => RecyclingMethod::Fast,
|
||||
PostgreSqlRecyclingMethod::Verified => RecyclingMethod::Verified,
|
||||
PostgreSqlRecyclingMethod::Clean => RecyclingMethod::Clean,
|
||||
},
|
||||
});
|
||||
if let Some(max_conn) = config.pool_max_connections {
|
||||
cfg.pool = PoolConfig::new(max_conn as usize).into();
|
||||
}
|
||||
|
||||
let primary_pool = if config.use_tls {
|
||||
cfg.create_pool(
|
||||
Some(Runtime::Tokio1),
|
||||
MakeRustlsConnect::new(rustls_client_config(config.allow_invalid_certs)?),
|
||||
)
|
||||
} else {
|
||||
cfg.create_pool(Some(Runtime::Tokio1), NoTls)
|
||||
}
|
||||
.map_err(|e| format!("Failed to create connection pool: {e}"))?;
|
||||
let ts_configs = discover_ts_configs(&primary_pool).await;
|
||||
|
||||
let mut replicas = vec![];
|
||||
for replica in config.read_replicas {
|
||||
let mut cfg = cfg.clone();
|
||||
cfg.dbname = replica.database.into();
|
||||
cfg.host = replica.host.into();
|
||||
cfg.user = replica.auth_username;
|
||||
cfg.password = replica.auth_secret.secret().await?.map(|v| v.into_owned());
|
||||
cfg.port = (replica.port as u16).into();
|
||||
cfg.options = replica.options;
|
||||
replicas.push(Store::PostgreSQL(Arc::new(PostgresStore {
|
||||
conn_pool: if config.use_tls {
|
||||
cfg.create_pool(
|
||||
Some(Runtime::Tokio1),
|
||||
MakeRustlsConnect::new(rustls_client_config(config.allow_invalid_certs)?),
|
||||
)
|
||||
} else {
|
||||
cfg.create_pool(Some(Runtime::Tokio1), NoTls)
|
||||
}
|
||||
.map_err(|e| format!("Failed to create connection pool: {e}"))?,
|
||||
ts_configs: ts_configs.clone(),
|
||||
})));
|
||||
}
|
||||
|
||||
let primary = Store::PostgreSQL(Arc::new(PostgresStore {
|
||||
conn_pool: primary_pool,
|
||||
ts_configs,
|
||||
}));
|
||||
|
||||
|
||||
Ok(primary)
|
||||
}
|
||||
|
||||
pub(crate) async fn create_storage_tables(&self) -> trc::Result<()> {
|
||||
let conn = self.conn_pool.get().await.map_err(into_pool_error)?;
|
||||
|
||||
for table in [
|
||||
SUBSPACE_ACL,
|
||||
SUBSPACE_TASK_QUEUE,
|
||||
SUBSPACE_DELETED_ITEMS,
|
||||
SUBSPACE_SPAM_SAMPLES,
|
||||
SUBSPACE_BLOB_LINK,
|
||||
SUBSPACE_IN_MEMORY_VALUE,
|
||||
SUBSPACE_PROPERTY,
|
||||
SUBSPACE_REGISTRY,
|
||||
SUBSPACE_REGISTRY_PK,
|
||||
SUBSPACE_QUEUE_MESSAGE,
|
||||
SUBSPACE_QUEUE_EVENT,
|
||||
SUBSPACE_REPORT_OUT,
|
||||
SUBSPACE_REPORT_IN,
|
||||
SUBSPACE_LOGS,
|
||||
SUBSPACE_BLOBS,
|
||||
SUBSPACE_DIRECTORY,
|
||||
SUBSPACE_TELEMETRY_SPAN,
|
||||
SUBSPACE_TELEMETRY_METRIC,
|
||||
] {
|
||||
let table = char::from(table);
|
||||
conn.execute(
|
||||
&format!(
|
||||
"CREATE TABLE IF NOT EXISTS {table} (
|
||||
k BYTEA PRIMARY KEY,
|
||||
v BYTEA NOT NULL
|
||||
)"
|
||||
),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
}
|
||||
|
||||
for table in [SUBSPACE_INDEXES, SUBSPACE_REGISTRY_IDX] {
|
||||
let table = char::from(table);
|
||||
conn.execute(
|
||||
&format!(
|
||||
"CREATE TABLE IF NOT EXISTS {table} (
|
||||
k BYTEA PRIMARY KEY
|
||||
)"
|
||||
),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
}
|
||||
|
||||
for table in [SUBSPACE_COUNTER, SUBSPACE_QUOTA, SUBSPACE_IN_MEMORY_COUNTER] {
|
||||
conn.execute(
|
||||
&format!(
|
||||
"CREATE TABLE IF NOT EXISTS {} (
|
||||
k BYTEA PRIMARY KEY,
|
||||
v BIGINT NOT NULL DEFAULT 0
|
||||
)",
|
||||
char::from(table)
|
||||
),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn create_search_tables(&self) -> trc::Result<()> {
|
||||
let conn = self.conn_pool.get().await.map_err(into_pool_error)?;
|
||||
|
||||
create_search_tables::<EmailSearchField>(&conn).await?;
|
||||
create_search_tables::<CalendarSearchField>(&conn).await?;
|
||||
create_search_tables::<ContactSearchField>(&conn).await?;
|
||||
//create_search_tables::<FileSearchField>(&conn).await?;
|
||||
create_search_tables::<TracingSearchField>(&conn).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_search_tables<T: SearchableField + PsqlSearchField + 'static>(
|
||||
conn: &Object,
|
||||
) -> trc::Result<()> {
|
||||
let table_name = T::index().psql_table();
|
||||
let mut query = format!("CREATE TABLE IF NOT EXISTS {} (", table_name);
|
||||
|
||||
// Add primary key columns
|
||||
let pkeys = T::primary_keys();
|
||||
for pkey in pkeys {
|
||||
query.push_str(&format!("{} {}, ", pkey.column(), pkey.column_type()));
|
||||
}
|
||||
|
||||
// Add other columns
|
||||
for field in T::all_fields() {
|
||||
query.push_str(&format!("{} {}", field.column(), field.column_type()));
|
||||
if let Some(sort_type) = field.sort_column_type() {
|
||||
query.push_str(&format!(", {} {}", field.sort_column().unwrap(), sort_type));
|
||||
}
|
||||
query.push_str(", ");
|
||||
}
|
||||
|
||||
// Add primary key constraint
|
||||
query.push_str("PRIMARY KEY (");
|
||||
for (i, pkey) in pkeys.iter().enumerate() {
|
||||
if i > 0 {
|
||||
query.push_str(", ");
|
||||
}
|
||||
query.push_str(pkey.column());
|
||||
}
|
||||
query.push_str("))");
|
||||
|
||||
conn.execute(&query, &[]).await.map_err(into_error)?;
|
||||
|
||||
// Create indexes
|
||||
for field in T::all_fields() {
|
||||
if field.is_text() || field.is_json() {
|
||||
let column_name = field.column();
|
||||
let create_index_query = format!(
|
||||
"CREATE INDEX IF NOT EXISTS gin_{table_name}_{column_name} ON {table_name} USING GIN({column_name})",
|
||||
);
|
||||
conn.execute(&create_index_query, &[])
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
}
|
||||
|
||||
if field.is_indexed() {
|
||||
let column_name = field.sort_column().unwrap_or(field.column());
|
||||
let create_index_query = format!(
|
||||
"CREATE INDEX IF NOT EXISTS idx_{table_name}_{column_name} ON {table_name}({column_name})",
|
||||
);
|
||||
conn.execute(&create_index_query, &[])
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn discover_ts_configs(pool: &Pool) -> AHashSet<&'static str> {
|
||||
let mut ts_configs = AHashSet::from_iter([PG_FALLBACK_LANG, PG_UNSTEMMED_LANG]);
|
||||
|
||||
match probe_ts_configs(pool).await {
|
||||
Ok(available) => {
|
||||
for name in available {
|
||||
if let Some(config) = PG_LANGS.iter().copied().find(|config| *config == name) {
|
||||
ts_configs.insert(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
trc::event!(
|
||||
Store(trc::StoreEvent::PostgresqlError),
|
||||
Details = "Failed to query pg_ts_config, assuming english only",
|
||||
Reason = err.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ts_configs
|
||||
}
|
||||
|
||||
async fn probe_ts_configs(pool: &Pool) -> trc::Result<Vec<String>> {
|
||||
let conn = pool.get().await.map_err(into_pool_error)?;
|
||||
|
||||
conn.query("SELECT cfgname::text FROM pg_ts_config", &[])
|
||||
.await
|
||||
.map_err(into_error)?
|
||||
.into_iter()
|
||||
.map(|row| row.try_get::<_, String>(0).map_err(into_error))
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
search::{
|
||||
CalendarSearchField, ContactSearchField, EmailSearchField, FileSearchField, SearchField,
|
||||
TracingSearchField,
|
||||
},
|
||||
write::SearchIndex,
|
||||
};
|
||||
use ahash::AHashSet;
|
||||
use deadpool_postgres::Pool;
|
||||
use tokio_postgres::error::SqlState;
|
||||
|
||||
pub mod blob;
|
||||
pub mod lookup;
|
||||
pub mod main;
|
||||
pub mod read;
|
||||
pub mod search;
|
||||
pub mod tls;
|
||||
pub mod write;
|
||||
|
||||
pub struct PostgresStore {
|
||||
pub(crate) conn_pool: Pool,
|
||||
pub(crate) ts_configs: AHashSet<&'static str>,
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn into_error(err: tokio_postgres::error::Error) -> trc::Error {
|
||||
let mut local_err = trc::StoreEvent::PostgresqlError.reason(error_chain(&err));
|
||||
if let Some(db_err) = err.as_db_error() {
|
||||
local_err = local_err.code(db_err.code().code().to_string());
|
||||
if let Some(detail) = db_err.detail() {
|
||||
local_err = local_err.details(detail.to_string());
|
||||
}
|
||||
|
||||
if let Some(hint) = db_err.hint() {
|
||||
local_err = local_err.caused_by(hint.to_string());
|
||||
}
|
||||
}
|
||||
local_err
|
||||
}
|
||||
|
||||
fn error_chain(err: &(dyn std::error::Error + 'static)) -> String {
|
||||
let mut message = err.to_string();
|
||||
let mut source = err.source();
|
||||
while let Some(cause) = source {
|
||||
let cause_message = cause.to_string();
|
||||
if !cause_message.is_empty() && !message.ends_with(&cause_message) {
|
||||
message.push_str(": ");
|
||||
message.push_str(&cause_message);
|
||||
}
|
||||
source = cause.source();
|
||||
}
|
||||
message
|
||||
}
|
||||
|
||||
pub(crate) const DELETE_CHUNK_SIZE: usize = 1000;
|
||||
pub(crate) const MIN_DELETE_CHUNK_SIZE: usize = 10;
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn is_timeout_error(err: &tokio_postgres::Error) -> bool {
|
||||
err.code().is_some_and(|code| {
|
||||
*code == SqlState::QUERY_CANCELED
|
||||
|| *code == SqlState::IDLE_IN_TRANSACTION_SESSION_TIMEOUT
|
||||
|| *code == SqlState::LOCK_NOT_AVAILABLE
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn into_pool_error(err: deadpool_postgres::PoolError) -> trc::Error {
|
||||
match err {
|
||||
deadpool_postgres::PoolError::Backend(err) => into_error(err),
|
||||
err => trc::StoreEvent::PostgresqlError.reason(error_chain(&err)),
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchIndex {
|
||||
pub fn psql_table(&self) -> &'static str {
|
||||
match self {
|
||||
SearchIndex::Email => "s_email",
|
||||
SearchIndex::Calendar => "s_cal",
|
||||
SearchIndex::Contacts => "s_card",
|
||||
SearchIndex::File => "s_file",
|
||||
SearchIndex::Tracing => "s_trace",
|
||||
SearchIndex::InMemory => "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait PsqlSearchField {
|
||||
fn column(&self) -> &'static str;
|
||||
fn column_type(&self) -> &'static str;
|
||||
fn sort_column_type(&self) -> Option<&'static str>;
|
||||
fn sort_column(&self) -> Option<&'static str>;
|
||||
}
|
||||
|
||||
impl PsqlSearchField for EmailSearchField {
|
||||
fn column(&self) -> &'static str {
|
||||
match self {
|
||||
EmailSearchField::From => "fadr",
|
||||
EmailSearchField::To => "tadr",
|
||||
EmailSearchField::Cc => "cc",
|
||||
EmailSearchField::Bcc => "bcc",
|
||||
EmailSearchField::Subject => "subj",
|
||||
EmailSearchField::Body => "body",
|
||||
EmailSearchField::Attachment => "atta",
|
||||
EmailSearchField::ReceivedAt => "rcvd",
|
||||
EmailSearchField::SentAt => "sent",
|
||||
EmailSearchField::Size => "size",
|
||||
EmailSearchField::HasAttachment => "hatt",
|
||||
EmailSearchField::Headers => "hdrs",
|
||||
}
|
||||
}
|
||||
|
||||
fn column_type(&self) -> &'static str {
|
||||
match self {
|
||||
EmailSearchField::ReceivedAt | EmailSearchField::SentAt => "BIGINT",
|
||||
EmailSearchField::Size => "INTEGER",
|
||||
EmailSearchField::HasAttachment => "BOOLEAN",
|
||||
EmailSearchField::Headers => "JSONB",
|
||||
_ => "TSVECTOR",
|
||||
}
|
||||
}
|
||||
|
||||
fn sort_column_type(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
EmailSearchField::From | EmailSearchField::To | EmailSearchField::Subject => {
|
||||
Some("TEXT")
|
||||
}
|
||||
#[cfg(feature = "test_mode")]
|
||||
EmailSearchField::Cc | EmailSearchField::Bcc => Some("TEXT"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn sort_column(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
EmailSearchField::From => Some("s_fr"),
|
||||
EmailSearchField::To => Some("s_to"),
|
||||
EmailSearchField::Subject => Some("s_sj"),
|
||||
#[cfg(feature = "test_mode")]
|
||||
EmailSearchField::Bcc => Some("s_bc"),
|
||||
#[cfg(feature = "test_mode")]
|
||||
EmailSearchField::Cc => Some("s_cc"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PsqlSearchField for CalendarSearchField {
|
||||
fn column(&self) -> &'static str {
|
||||
match self {
|
||||
CalendarSearchField::Title => "titl",
|
||||
CalendarSearchField::Description => "dscd",
|
||||
CalendarSearchField::Location => "locn",
|
||||
CalendarSearchField::Owner => "ownr",
|
||||
CalendarSearchField::Attendee => "atnd",
|
||||
CalendarSearchField::Start => "strt",
|
||||
CalendarSearchField::Uid => "uid",
|
||||
}
|
||||
}
|
||||
|
||||
fn column_type(&self) -> &'static str {
|
||||
match self {
|
||||
CalendarSearchField::Start => "BIGINT",
|
||||
CalendarSearchField::Uid => "TEXT",
|
||||
_ => "TSVECTOR",
|
||||
}
|
||||
}
|
||||
|
||||
fn sort_column_type(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn sort_column(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl PsqlSearchField for ContactSearchField {
|
||||
fn column(&self) -> &'static str {
|
||||
match self {
|
||||
ContactSearchField::Member => "mmbr",
|
||||
ContactSearchField::Name => "name",
|
||||
ContactSearchField::Nickname => "nick",
|
||||
ContactSearchField::Organization => "orgn",
|
||||
ContactSearchField::Email => "eml",
|
||||
ContactSearchField::Phone => "phon",
|
||||
ContactSearchField::OnlineService => "olsv",
|
||||
ContactSearchField::Address => "addr",
|
||||
ContactSearchField::Note => "note",
|
||||
ContactSearchField::Kind => "kind",
|
||||
ContactSearchField::Uid => "uid",
|
||||
}
|
||||
}
|
||||
|
||||
fn column_type(&self) -> &'static str {
|
||||
match self {
|
||||
ContactSearchField::Kind | ContactSearchField::Uid => "TEXT",
|
||||
_ => "TSVECTOR",
|
||||
}
|
||||
}
|
||||
|
||||
fn sort_column_type(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn sort_column(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl PsqlSearchField for FileSearchField {
|
||||
fn column(&self) -> &'static str {
|
||||
match self {
|
||||
FileSearchField::Name => "name",
|
||||
FileSearchField::Content => "body",
|
||||
}
|
||||
}
|
||||
|
||||
fn column_type(&self) -> &'static str {
|
||||
"TSVECTOR"
|
||||
}
|
||||
|
||||
fn sort_column_type(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn sort_column(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
}
|
||||
impl PsqlSearchField for TracingSearchField {
|
||||
fn column(&self) -> &'static str {
|
||||
match self {
|
||||
TracingSearchField::QueueId => "qid",
|
||||
TracingSearchField::EventType => "etyp",
|
||||
TracingSearchField::Keywords => "kwds",
|
||||
}
|
||||
}
|
||||
|
||||
fn column_type(&self) -> &'static str {
|
||||
match self {
|
||||
TracingSearchField::EventType => "BIGINT",
|
||||
TracingSearchField::QueueId => "BIGINT",
|
||||
TracingSearchField::Keywords => "TSVECTOR",
|
||||
}
|
||||
}
|
||||
|
||||
fn sort_column_type(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn sort_column(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl PsqlSearchField for SearchField {
|
||||
fn column(&self) -> &'static str {
|
||||
match self {
|
||||
SearchField::AccountId => "accid",
|
||||
SearchField::DocumentId => "docid",
|
||||
SearchField::Id => "id",
|
||||
SearchField::Email(field) => field.column(),
|
||||
SearchField::Calendar(field) => field.column(),
|
||||
SearchField::Contact(field) => field.column(),
|
||||
SearchField::File(field) => field.column(),
|
||||
SearchField::Tracing(field) => field.column(),
|
||||
}
|
||||
}
|
||||
|
||||
fn column_type(&self) -> &'static str {
|
||||
match self {
|
||||
SearchField::AccountId => "INTEGER NOT NULL",
|
||||
SearchField::DocumentId => "INTEGER NOT NULL",
|
||||
SearchField::Id => "BIGINT NOT NULL",
|
||||
SearchField::Email(field) => field.column_type(),
|
||||
SearchField::Calendar(field) => field.column_type(),
|
||||
SearchField::Contact(field) => field.column_type(),
|
||||
SearchField::File(field) => field.column_type(),
|
||||
SearchField::Tracing(field) => field.column_type(),
|
||||
}
|
||||
}
|
||||
|
||||
fn sort_column_type(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
SearchField::Email(field) => field.sort_column_type(),
|
||||
SearchField::Calendar(field) => field.sort_column_type(),
|
||||
SearchField::Contact(field) => field.sort_column_type(),
|
||||
SearchField::File(field) => field.sort_column_type(),
|
||||
SearchField::Tracing(field) => field.sort_column_type(),
|
||||
SearchField::AccountId | SearchField::DocumentId | SearchField::Id => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn sort_column(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
SearchField::Email(field) => field.sort_column(),
|
||||
SearchField::Calendar(field) => field.sort_column(),
|
||||
SearchField::Contact(field) => field.sort_column(),
|
||||
SearchField::File(field) => field.sort_column(),
|
||||
SearchField::Tracing(field) => field.sort_column(),
|
||||
SearchField::AccountId | SearchField::DocumentId | SearchField::Id => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{PostgresStore, into_error, is_timeout_error};
|
||||
use crate::{
|
||||
Deserialize, IterateParams, Key, ValueKey, backend::postgres::into_pool_error,
|
||||
write::ValueClass,
|
||||
};
|
||||
use futures::{TryStreamExt, pin_mut};
|
||||
|
||||
impl PostgresStore {
|
||||
pub(crate) async fn get_value<U>(&self, key: impl Key) -> trc::Result<Option<U>>
|
||||
where
|
||||
U: Deserialize + 'static,
|
||||
{
|
||||
let conn = self.conn_pool.get().await.map_err(into_pool_error)?;
|
||||
let s = conn
|
||||
.prepare_cached(&format!(
|
||||
"SELECT v FROM {} WHERE k = $1",
|
||||
char::from(key.subspace())
|
||||
))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
let key = key.serialize(0);
|
||||
conn.query_opt(&s, &[&key])
|
||||
.await
|
||||
.map_err(into_error)
|
||||
.and_then(|r| {
|
||||
if let Some(r) = r {
|
||||
Ok(Some(U::deserialize_with_key(&key, r.get(0))?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn key_exists(&self, key: impl Key) -> trc::Result<bool> {
|
||||
let conn = self.conn_pool.get().await.map_err(into_pool_error)?;
|
||||
let s = conn
|
||||
.prepare_cached(&format!(
|
||||
"SELECT 1 FROM {} WHERE k = $1",
|
||||
char::from(key.subspace())
|
||||
))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
let key = key.serialize(0);
|
||||
conn.query_opt(&s, &[&key])
|
||||
.await
|
||||
.map_err(into_error)
|
||||
.map(|r| r.is_some())
|
||||
}
|
||||
|
||||
pub(crate) async fn iterate<T: Key>(
|
||||
&self,
|
||||
params: IterateParams<T>,
|
||||
mut cb: impl for<'x> FnMut(&'x [u8], &'x [u8]) -> trc::Result<bool> + Sync + Send,
|
||||
) -> trc::Result<()> {
|
||||
let conn = self.conn_pool.get().await.map_err(into_pool_error)?;
|
||||
let table = char::from(params.begin.subspace());
|
||||
let begin = params.begin.serialize(0);
|
||||
let end = params.end.serialize(0);
|
||||
let keys = if params.values { "k, v" } else { "k" };
|
||||
|
||||
let s = conn
|
||||
.prepare_cached(&match (params.first, params.ascending) {
|
||||
(true, true) => {
|
||||
format!(
|
||||
"SELECT {keys} FROM {table} WHERE k >= $1 AND k <= $2 ORDER BY k ASC LIMIT 1"
|
||||
)
|
||||
}
|
||||
(true, false) => {
|
||||
format!(
|
||||
"SELECT {keys} FROM {table} WHERE k >= $1 AND k <= $2 ORDER BY k DESC LIMIT 1"
|
||||
)
|
||||
}
|
||||
(false, true) => {
|
||||
format!("SELECT {keys} FROM {table} WHERE k >= $1 AND k <= $2 ORDER BY k ASC")
|
||||
}
|
||||
(false, false) => {
|
||||
format!("SELECT {keys} FROM {table} WHERE k >= $1 AND k <= $2 ORDER BY k DESC")
|
||||
}
|
||||
})
|
||||
.await.map_err(into_error)?;
|
||||
let mut from = begin;
|
||||
let mut to = end;
|
||||
let mut resume_key: Option<Vec<u8>> = None;
|
||||
|
||||
loop {
|
||||
let mut last_key = None;
|
||||
let mut timed_out = false;
|
||||
|
||||
{
|
||||
let rows = conn
|
||||
.query_raw(&s, &[&from, &to])
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
|
||||
pin_mut!(rows);
|
||||
|
||||
loop {
|
||||
match rows.try_next().await {
|
||||
Ok(Some(row)) => {
|
||||
let key = row.try_get::<_, &[u8]>(0).map_err(into_error)?;
|
||||
let value = if params.values {
|
||||
row.try_get::<_, &[u8]>(1).map_err(into_error)?
|
||||
} else {
|
||||
b"".as_slice()
|
||||
};
|
||||
|
||||
if resume_key.take().is_some_and(|resumed| resumed == key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !cb(key, value)? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
last_key = Some(key.to_vec());
|
||||
}
|
||||
Ok(None) => break,
|
||||
Err(err) => {
|
||||
if params.first || last_key.is_none() || !is_timeout_error(&err) {
|
||||
return Err(into_error(err));
|
||||
}
|
||||
timed_out = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match last_key {
|
||||
Some(last_key) if timed_out => {
|
||||
if params.ascending {
|
||||
from.clone_from(&last_key);
|
||||
} else {
|
||||
to.clone_from(&last_key);
|
||||
}
|
||||
resume_key = Some(last_key);
|
||||
}
|
||||
_ => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_counter(
|
||||
&self,
|
||||
key: impl Into<ValueKey<ValueClass>> + Sync + Send,
|
||||
) -> trc::Result<i64> {
|
||||
let key = key.into();
|
||||
let table = char::from(key.subspace());
|
||||
let key = key.serialize(0);
|
||||
|
||||
let conn = self.conn_pool.get().await.map_err(into_pool_error)?;
|
||||
let s = conn
|
||||
.prepare_cached(&format!("SELECT v FROM {table} WHERE k = $1"))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
match conn.query_opt(&s, &[&key]).await {
|
||||
Ok(Some(row)) => row.try_get(0).map_err(into_error),
|
||||
Ok(None) => Ok(0),
|
||||
Err(e) => Err(into_error(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
backend::postgres::{
|
||||
DELETE_CHUNK_SIZE, MIN_DELETE_CHUNK_SIZE, PostgresStore, PsqlSearchField, into_error,
|
||||
into_pool_error, is_timeout_error,
|
||||
},
|
||||
search::{
|
||||
IndexDocument, SearchComparator, SearchDocumentId, SearchFilter, SearchOperator,
|
||||
SearchQuery, SearchValue,
|
||||
},
|
||||
write::SearchIndex,
|
||||
};
|
||||
use nlp::language::Language;
|
||||
use std::fmt::Write;
|
||||
use tokio_postgres::{
|
||||
IsolationLevel,
|
||||
types::{FromSql, ToSql, Type, WrongType},
|
||||
};
|
||||
|
||||
impl PostgresStore {
|
||||
fn ts_config(&self, language: &Language) -> &'static str {
|
||||
pg_lang(language)
|
||||
.filter(|config| self.ts_configs.contains(config))
|
||||
.unwrap_or(PG_UNSTEMMED_LANG)
|
||||
}
|
||||
|
||||
pub async fn index(&self, documents: Vec<IndexDocument>) -> trc::Result<()> {
|
||||
let mut conn = self.conn_pool.get().await.map_err(into_pool_error)?;
|
||||
let trx = conn
|
||||
.build_transaction()
|
||||
.isolation_level(IsolationLevel::ReadCommitted)
|
||||
.start()
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
|
||||
for document in documents {
|
||||
let index = document.index;
|
||||
let primary_keys = index.primary_keys();
|
||||
let all_fields = index.all_fields();
|
||||
let fields = document.fields;
|
||||
let mut values = Vec::with_capacity(fields.len() + 2);
|
||||
let mut query = format!("INSERT INTO {} (", index.psql_table());
|
||||
|
||||
for (i, field) in primary_keys.iter().chain(all_fields).enumerate() {
|
||||
if i > 0 {
|
||||
query.push(',');
|
||||
}
|
||||
query.push_str(field.column());
|
||||
|
||||
if let Some(sort_column) = field.sort_column() {
|
||||
query.push(',');
|
||||
query.push_str(sort_column);
|
||||
}
|
||||
}
|
||||
|
||||
query.push_str(") VALUES (");
|
||||
|
||||
for (i, field) in primary_keys.iter().chain(all_fields).enumerate() {
|
||||
if i > 0 {
|
||||
query.push(',');
|
||||
}
|
||||
|
||||
if let Some(value) = fields.get(field) {
|
||||
let value_ref = format!("${}", values.len() + 1);
|
||||
let (text_len, language) = if let SearchValue::Text { value, language } = value
|
||||
{
|
||||
(value.len(), self.ts_config(language))
|
||||
} else {
|
||||
(0, PG_UNSTEMMED_LANG)
|
||||
};
|
||||
|
||||
if field.is_text() {
|
||||
let _ = write!(&mut query, "to_tsvector('{language}',{value_ref})");
|
||||
} else if text_len > 512 {
|
||||
query.push_str("left(");
|
||||
query.push_str(&value_ref);
|
||||
query.push_str(",512)");
|
||||
} else {
|
||||
query.push_str(&value_ref);
|
||||
}
|
||||
|
||||
if field.sort_column().is_some() {
|
||||
if text_len > 255 {
|
||||
query.push_str(",left(");
|
||||
query.push_str(&value_ref);
|
||||
query.push_str(",255)");
|
||||
} else {
|
||||
query.push(',');
|
||||
query.push_str(&value_ref);
|
||||
}
|
||||
}
|
||||
|
||||
values.push(value as &(dyn ToSql + Sync));
|
||||
} else {
|
||||
query.push_str("NULL");
|
||||
if field.sort_column().is_some() {
|
||||
query.push_str(",NULL");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query.push_str(") ON CONFLICT (");
|
||||
for (i, pkey) in primary_keys.iter().enumerate() {
|
||||
if i > 0 {
|
||||
query.push(',');
|
||||
}
|
||||
query.push_str(pkey.column());
|
||||
}
|
||||
query.push_str(") DO UPDATE SET ");
|
||||
for (i, field) in all_fields.iter().enumerate() {
|
||||
if i > 0 {
|
||||
query.push(',');
|
||||
}
|
||||
let column = field.column();
|
||||
let _ = write!(&mut query, "{column} = EXCLUDED.{column}");
|
||||
}
|
||||
|
||||
trx.execute(&query, &values).await.map_err(into_error)?;
|
||||
}
|
||||
|
||||
trx.commit().await.map_err(into_error)
|
||||
}
|
||||
|
||||
pub async fn query<R: SearchDocumentId>(
|
||||
&self,
|
||||
index: SearchIndex,
|
||||
filters: &[SearchFilter],
|
||||
sort: &[SearchComparator],
|
||||
) -> trc::Result<Vec<R>> {
|
||||
let mut query = format!("SELECT {} FROM {}", R::field().column(), index.psql_table());
|
||||
let params = self.build_filter(&mut query, filters);
|
||||
if !sort.is_empty() {
|
||||
build_sort(&mut query, sort);
|
||||
}
|
||||
let conn = self.conn_pool.get().await.map_err(into_pool_error)?;
|
||||
let s = conn.prepare_cached(&query).await.map_err(into_error)?;
|
||||
|
||||
conn.query(&s, params.as_slice())
|
||||
.await
|
||||
.and_then(|rows| {
|
||||
rows.into_iter()
|
||||
.map(|row| row.try_get::<_, DocId>(0).map(|v| R::from_u64(v.0)))
|
||||
.collect::<Result<Vec<R>, _>>()
|
||||
})
|
||||
.map_err(into_error)
|
||||
}
|
||||
|
||||
pub async fn unindex(&self, filter: SearchQuery) -> trc::Result<u64> {
|
||||
debug_assert!(!filter.filters.is_empty());
|
||||
let table = filter.index.psql_table();
|
||||
let mut where_clause = String::new();
|
||||
let params = self.build_filter(&mut where_clause, &filter.filters);
|
||||
let conn = self.conn_pool.get().await.map_err(into_pool_error)?;
|
||||
let s = conn
|
||||
.prepare_cached(&format!("DELETE FROM {table}{where_clause}"))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
|
||||
match conn.execute(&s, params.as_slice()).await {
|
||||
Ok(deleted) => return Ok(deleted),
|
||||
Err(err) if is_timeout_error(&err) => (),
|
||||
Err(err) => return Err(into_error(err)),
|
||||
}
|
||||
|
||||
let mut chunk_size = DELETE_CHUNK_SIZE;
|
||||
let mut deleted = 0;
|
||||
|
||||
loop {
|
||||
let s = conn
|
||||
.prepare_cached(&format!(
|
||||
"DELETE FROM {table} WHERE ctid IN (SELECT ctid FROM {table}{where_clause} LIMIT {chunk_size})"
|
||||
))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
|
||||
loop {
|
||||
match conn.execute(&s, params.as_slice()).await {
|
||||
Ok(0) => return Ok(deleted),
|
||||
Ok(affected) => deleted += affected,
|
||||
Err(err) if is_timeout_error(&err) && chunk_size > MIN_DELETE_CHUNK_SIZE => {
|
||||
chunk_size = (chunk_size / 2).max(MIN_DELETE_CHUNK_SIZE);
|
||||
break;
|
||||
}
|
||||
Err(err) => return Err(into_error(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_filter<'x>(
|
||||
&self,
|
||||
query: &mut String,
|
||||
filters: &'x [SearchFilter],
|
||||
) -> Vec<&'x (dyn ToSql + Sync)> {
|
||||
if filters.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
query.push_str(" WHERE ");
|
||||
let mut operator_stack = Vec::new();
|
||||
let mut operator = &SearchFilter::And;
|
||||
let mut is_first = true;
|
||||
let mut values = Vec::new();
|
||||
|
||||
for filter in filters {
|
||||
match filter {
|
||||
SearchFilter::Operator { field, op, value } => {
|
||||
if !is_first {
|
||||
match operator {
|
||||
SearchFilter::And => query.push_str(" AND "),
|
||||
SearchFilter::Or => query.push_str(" OR "),
|
||||
_ => (),
|
||||
}
|
||||
} else {
|
||||
is_first = false;
|
||||
}
|
||||
let value_pos = values.len() + 1;
|
||||
if field.is_text()
|
||||
&& matches!(op, SearchOperator::Equal | SearchOperator::Contains)
|
||||
{
|
||||
query.push_str(field.column());
|
||||
query.push(' ');
|
||||
|
||||
let language = match &value {
|
||||
SearchValue::Text { language, .. } => *language,
|
||||
_ => Language::None,
|
||||
};
|
||||
let config = self.ts_config(&language);
|
||||
let method = match op {
|
||||
SearchOperator::Equal => "phraseto_tsquery",
|
||||
_ => "plainto_tsquery",
|
||||
};
|
||||
|
||||
if matches!(language, Language::None) {
|
||||
let _ = write!(query, "@@ {method}('{config}', ${value_pos})");
|
||||
} else {
|
||||
let _ = write!(query, "@@ ({method}('{config}', ${value_pos})");
|
||||
for fallback in [PG_FALLBACK_LANG, PG_UNSTEMMED_LANG] {
|
||||
if fallback != config && self.ts_configs.contains(fallback) {
|
||||
let _ =
|
||||
write!(query, " || {method}('{fallback}', ${value_pos})");
|
||||
}
|
||||
}
|
||||
query.push(')');
|
||||
}
|
||||
values.push(value as &(dyn ToSql + Sync));
|
||||
} else if let SearchValue::KeyValues(kv) = value {
|
||||
query.push_str(field.column());
|
||||
query.push(' ');
|
||||
|
||||
let (key, value) = kv.iter().next().unwrap();
|
||||
values.push(key as &(dyn ToSql + Sync));
|
||||
|
||||
if !value.is_empty() {
|
||||
let _ = write!(query, "->> ${value_pos} ");
|
||||
op.write_pqsql(query, values.len() + 1);
|
||||
values.push(value as &(dyn ToSql + Sync));
|
||||
} else {
|
||||
let _ = write!(query, " ? ${value_pos}");
|
||||
}
|
||||
} else {
|
||||
query.push_str(field.sort_column().unwrap_or(field.column()));
|
||||
query.push(' ');
|
||||
|
||||
op.write_pqsql(query, value_pos);
|
||||
values.push(value as &(dyn ToSql + Sync));
|
||||
}
|
||||
}
|
||||
SearchFilter::And | SearchFilter::Or => {
|
||||
if !is_first {
|
||||
match operator {
|
||||
SearchFilter::And => query.push_str(" AND "),
|
||||
SearchFilter::Or => query.push_str(" OR "),
|
||||
_ => (),
|
||||
}
|
||||
} else {
|
||||
is_first = false;
|
||||
}
|
||||
|
||||
operator_stack.push((operator, is_first));
|
||||
operator = filter;
|
||||
is_first = true;
|
||||
query.push('(');
|
||||
}
|
||||
SearchFilter::Not => {
|
||||
if !is_first {
|
||||
match operator {
|
||||
SearchFilter::And => query.push_str(" AND "),
|
||||
SearchFilter::Or => query.push_str(" OR "),
|
||||
_ => (),
|
||||
}
|
||||
} else {
|
||||
is_first = false;
|
||||
}
|
||||
|
||||
operator_stack.push((operator, is_first));
|
||||
operator = &SearchFilter::And;
|
||||
is_first = true;
|
||||
query.push_str("NOT (");
|
||||
}
|
||||
SearchFilter::End => {
|
||||
let p = operator_stack.pop().unwrap_or((&SearchFilter::And, true));
|
||||
operator = p.0;
|
||||
is_first = p.1;
|
||||
query.push(')');
|
||||
}
|
||||
SearchFilter::DocumentSet(_) => {
|
||||
debug_assert!(
|
||||
false,
|
||||
"DocumentSet filters are not supported in Postgres backend"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
values
|
||||
}
|
||||
}
|
||||
|
||||
fn build_sort(query: &mut String, sort: &[SearchComparator]) {
|
||||
query.push_str(" ORDER BY ");
|
||||
for (i, comparator) in sort.iter().enumerate() {
|
||||
if i > 0 {
|
||||
query.push_str(", ");
|
||||
}
|
||||
match comparator {
|
||||
SearchComparator::Field { field, ascending } => {
|
||||
query.push_str(field.sort_column().unwrap_or(field.column()));
|
||||
if *ascending {
|
||||
query.push_str(" ASC");
|
||||
} else {
|
||||
query.push_str(" DESC");
|
||||
}
|
||||
}
|
||||
SearchComparator::DocumentSet { .. } | SearchComparator::SortedSet { .. } => {
|
||||
debug_assert!(
|
||||
false,
|
||||
"DocumentSet and SortedSet comparators are not supported "
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql for SearchValue {
|
||||
fn to_sql(
|
||||
&self,
|
||||
ty: &tokio_postgres::types::Type,
|
||||
out: &mut bytes::BytesMut,
|
||||
) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match self {
|
||||
SearchValue::Text { value, .. } => {
|
||||
// Truncate large text fields to avoid Postgres errors (see https://www.postgresql.org/docs/current/textsearch-limitations.html)
|
||||
|
||||
if value.len() > 650_000 {
|
||||
(&value[..value.floor_char_boundary(650_000)]).to_sql(ty, out)
|
||||
} else {
|
||||
value.to_sql(ty, out)
|
||||
}
|
||||
}
|
||||
SearchValue::Int(v) => match *ty {
|
||||
Type::INT4 => (*v as i32).to_sql(ty, out),
|
||||
_ => v.to_sql(ty, out),
|
||||
},
|
||||
SearchValue::Uint(v) => match *ty {
|
||||
Type::INT4 => (*v as i32).to_sql(ty, out),
|
||||
_ => (*v as i64).to_sql(ty, out),
|
||||
},
|
||||
SearchValue::Boolean(v) => v.to_sql(ty, out),
|
||||
SearchValue::KeyValues(kv) => {
|
||||
serde_json::to_value(kv).unwrap_or_default().to_sql(ty, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts(_: &tokio_postgres::types::Type) -> bool
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
fn to_sql_checked(
|
||||
&self,
|
||||
ty: &tokio_postgres::types::Type,
|
||||
out: &mut bytes::BytesMut,
|
||||
) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
|
||||
match self {
|
||||
SearchValue::Text { value, .. } => {
|
||||
// Truncate large text fields to avoid Postgres errors (see https://www.postgresql.org/docs/current/textsearch-limitations.html)
|
||||
|
||||
if value.len() > 650_000 {
|
||||
(&value[..value.floor_char_boundary(650_000)]).to_sql_checked(ty, out)
|
||||
} else {
|
||||
value.to_sql_checked(ty, out)
|
||||
}
|
||||
}
|
||||
SearchValue::Int(v) => match *ty {
|
||||
Type::INT4 => (*v as i32).to_sql_checked(ty, out),
|
||||
_ => v.to_sql_checked(ty, out),
|
||||
},
|
||||
SearchValue::Uint(v) => match *ty {
|
||||
Type::INT4 => (*v as i32).to_sql_checked(ty, out),
|
||||
_ => (*v as i64).to_sql_checked(ty, out),
|
||||
},
|
||||
SearchValue::Boolean(v) => v.to_sql_checked(ty, out),
|
||||
SearchValue::KeyValues(kv) => serde_json::to_value(kv)
|
||||
.unwrap_or_default()
|
||||
.to_sql_checked(ty, out),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DocId(u64);
|
||||
|
||||
impl FromSql<'_> for DocId {
|
||||
fn from_sql(
|
||||
ty: &tokio_postgres::types::Type,
|
||||
raw: &'_ [u8],
|
||||
) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
|
||||
match ty {
|
||||
&Type::INT4 => i32::from_sql(ty, raw).map(|v| DocId(v as u64)),
|
||||
&Type::INT8 | &Type::OID => i64::from_sql(ty, raw).map(|v| DocId(v as u64)),
|
||||
_ => Err(Box::new(WrongType::new::<DocId>(ty.clone()))),
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts(typ: &Type) -> bool {
|
||||
matches!(typ, &Type::INT4 | &Type::INT8 | &Type::OID)
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchOperator {
|
||||
fn write_pqsql(&self, query: &mut String, value_pos: usize) {
|
||||
match self {
|
||||
SearchOperator::LowerThan => {
|
||||
let _ = write!(query, "< ${value_pos}");
|
||||
}
|
||||
SearchOperator::LowerEqualThan => {
|
||||
let _ = write!(query, "<= ${value_pos}");
|
||||
}
|
||||
SearchOperator::GreaterThan => {
|
||||
let _ = write!(query, "> ${value_pos}");
|
||||
}
|
||||
SearchOperator::GreaterEqualThan => {
|
||||
let _ = write!(query, ">= ${value_pos}");
|
||||
}
|
||||
SearchOperator::Equal => {
|
||||
let _ = write!(query, "= ${value_pos}");
|
||||
}
|
||||
SearchOperator::Contains => {
|
||||
let _ = write!(query, "LIKE '%' || ${value_pos} || '%'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) const PG_FALLBACK_LANG: &str = "english";
|
||||
pub(super) const PG_UNSTEMMED_LANG: &str = "simple";
|
||||
|
||||
pub(super) const PG_LANGS: &[&str] = &[
|
||||
"arabic",
|
||||
"armenian",
|
||||
"catalan",
|
||||
"danish",
|
||||
"dutch",
|
||||
"english",
|
||||
"finnish",
|
||||
"french",
|
||||
"german",
|
||||
"greek",
|
||||
"hindi",
|
||||
"hungarian",
|
||||
"indonesian",
|
||||
"italian",
|
||||
"lithuanian",
|
||||
"nepali",
|
||||
"norwegian",
|
||||
"portuguese",
|
||||
"romanian",
|
||||
"russian",
|
||||
"serbian",
|
||||
"spanish",
|
||||
"swedish",
|
||||
"tamil",
|
||||
"turkish",
|
||||
"yiddish",
|
||||
];
|
||||
|
||||
#[inline(always)]
|
||||
fn pg_lang(lang: &Language) -> Option<&'static str> {
|
||||
match lang {
|
||||
Language::Esperanto => None,
|
||||
Language::English => Some("english"),
|
||||
Language::Russian => Some("russian"),
|
||||
Language::Mandarin => None,
|
||||
Language::Spanish => Some("spanish"),
|
||||
Language::Portuguese => Some("portuguese"),
|
||||
Language::Italian => Some("italian"),
|
||||
Language::Bengali => None,
|
||||
Language::French => Some("french"),
|
||||
Language::German => Some("german"),
|
||||
Language::Ukrainian => None,
|
||||
Language::Georgian => None,
|
||||
Language::Arabic => Some("arabic"),
|
||||
Language::Hindi => Some("hindi"),
|
||||
Language::Japanese => None,
|
||||
Language::Hebrew => None,
|
||||
Language::Yiddish => Some("yiddish"),
|
||||
Language::Polish => None,
|
||||
Language::Amharic => None,
|
||||
Language::Javanese => None,
|
||||
Language::Korean => None,
|
||||
Language::Bokmal => Some("norwegian"), // Norwegian covers Bokmål
|
||||
Language::Danish => Some("danish"),
|
||||
Language::Swedish => Some("swedish"),
|
||||
Language::Finnish => Some("finnish"),
|
||||
Language::Turkish => Some("turkish"),
|
||||
Language::Dutch => Some("dutch"),
|
||||
Language::Hungarian => Some("hungarian"),
|
||||
Language::Czech => None,
|
||||
Language::Greek => Some("greek"),
|
||||
Language::Bulgarian => None,
|
||||
Language::Belarusian => None,
|
||||
Language::Marathi => None,
|
||||
Language::Kannada => None,
|
||||
Language::Romanian => Some("romanian"),
|
||||
Language::Slovene => None,
|
||||
Language::Croatian => None,
|
||||
Language::Serbian => Some("serbian"),
|
||||
Language::Macedonian => None,
|
||||
Language::Lithuanian => Some("lithuanian"),
|
||||
Language::Latvian => None,
|
||||
Language::Estonian => None,
|
||||
Language::Tamil => Some("tamil"),
|
||||
Language::Vietnamese => None,
|
||||
Language::Urdu => None,
|
||||
Language::Thai => None,
|
||||
Language::Gujarati => None,
|
||||
Language::Uzbek => None,
|
||||
Language::Punjabi => None,
|
||||
Language::Azerbaijani => None,
|
||||
Language::Indonesian => Some("indonesian"),
|
||||
Language::Telugu => None,
|
||||
Language::Persian => None,
|
||||
Language::Malayalam => None,
|
||||
Language::Oriya => None,
|
||||
Language::Burmese => None,
|
||||
Language::Nepali => Some("nepali"),
|
||||
Language::Sinhalese => None,
|
||||
Language::Khmer => None,
|
||||
Language::Turkmen => None,
|
||||
Language::Akan => None,
|
||||
Language::Zulu => None,
|
||||
Language::Shona => None,
|
||||
Language::Afrikaans => None,
|
||||
Language::Latin => None,
|
||||
Language::Slovak => None,
|
||||
Language::Catalan => Some("catalan"),
|
||||
Language::Tagalog => None,
|
||||
Language::Armenian => Some("armenian"),
|
||||
Language::Unknown | Language::None => None,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
// Credits: https://github.com/jbg/tokio-postgres-rustls
|
||||
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
future::Future,
|
||||
io,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use aws_lc_rs::digest;
|
||||
use futures::future::{FutureExt, TryFutureExt};
|
||||
use rustls::ClientConfig;
|
||||
use rustls_pki_types::ServerName;
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
use tokio_postgres::tls::{ChannelBinding, MakeTlsConnect, TlsConnect};
|
||||
use tokio_rustls::{TlsConnector, client::TlsStream};
|
||||
use x509_parser::{
|
||||
asn1_rs::oid,
|
||||
oid_registry::{
|
||||
OID_HASH_SHA1, OID_MD5_WITH_RSA, OID_NIST_HASH_SHA256, OID_NIST_HASH_SHA384,
|
||||
OID_NIST_HASH_SHA512, OID_PKCS1_MD5WITHRSAENC, OID_PKCS1_RSASSAPSS, OID_PKCS1_SHA1WITHRSA,
|
||||
OID_PKCS1_SHA224WITHRSA, OID_PKCS1_SHA256WITHRSA, OID_PKCS1_SHA384WITHRSA,
|
||||
OID_PKCS1_SHA512WITHRSA, OID_SHA1_WITH_RSA, OID_SIG_DSA_WITH_SHA1,
|
||||
OID_SIG_ECDSA_WITH_SHA224, OID_SIG_ECDSA_WITH_SHA256, OID_SIG_ECDSA_WITH_SHA384,
|
||||
OID_SIG_ECDSA_WITH_SHA512,
|
||||
},
|
||||
parse_x509_certificate,
|
||||
prelude::X509Certificate,
|
||||
signature_algorithm::RsaSsaPssParams,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MakeRustlsConnect {
|
||||
config: Arc<ClientConfig>,
|
||||
}
|
||||
|
||||
impl MakeRustlsConnect {
|
||||
pub fn new(config: ClientConfig) -> Self {
|
||||
Self {
|
||||
config: Arc::new(config),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> MakeTlsConnect<S> for MakeRustlsConnect
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
type Stream = RustlsStream<S>;
|
||||
type TlsConnect = RustlsConnect;
|
||||
type Error = io::Error;
|
||||
|
||||
fn make_tls_connect(&mut self, hostname: &str) -> io::Result<RustlsConnect> {
|
||||
ServerName::try_from(hostname.to_string())
|
||||
.map(|dns_name| {
|
||||
RustlsConnect(Some(RustlsConnectData {
|
||||
hostname: dns_name,
|
||||
connector: Arc::clone(&self.config).into(),
|
||||
}))
|
||||
})
|
||||
.or(Ok(RustlsConnect(None)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RustlsConnect(Option<RustlsConnectData>);
|
||||
|
||||
struct RustlsConnectData {
|
||||
hostname: ServerName<'static>,
|
||||
connector: TlsConnector,
|
||||
}
|
||||
|
||||
impl<S> TlsConnect<S> for RustlsConnect
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
type Stream = RustlsStream<S>;
|
||||
type Error = io::Error;
|
||||
type Future = Pin<Box<dyn Future<Output = io::Result<RustlsStream<S>>> + Send>>;
|
||||
|
||||
fn connect(self, stream: S) -> Self::Future {
|
||||
match self.0 {
|
||||
None => Box::pin(core::future::ready(Err(io::ErrorKind::InvalidInput.into()))),
|
||||
Some(c) => c
|
||||
.connector
|
||||
.connect(c.hostname, stream)
|
||||
.map_ok(|s| RustlsStream(Box::pin(s)))
|
||||
.boxed(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RustlsStream<S>(Pin<Box<TlsStream<S>>>);
|
||||
|
||||
fn cb_digest_for_cert(cert: &X509Certificate<'_>) -> Option<&'static digest::Algorithm> {
|
||||
let sig_alg = cert.signature_algorithm.oid();
|
||||
// Signature algorithms that use a digest should use the same digest for channel binding:
|
||||
if sig_alg == &OID_PKCS1_SHA512WITHRSA || sig_alg == &OID_SIG_ECDSA_WITH_SHA512 {
|
||||
Some(&digest::SHA512)
|
||||
} else if sig_alg == &OID_PKCS1_SHA384WITHRSA || sig_alg == &OID_SIG_ECDSA_WITH_SHA384 {
|
||||
Some(&digest::SHA384)
|
||||
} else if sig_alg == &OID_PKCS1_MD5WITHRSAENC
|
||||
|| sig_alg == &OID_MD5_WITH_RSA
|
||||
|| sig_alg == &OID_PKCS1_SHA1WITHRSA
|
||||
|| sig_alg == &OID_SHA1_WITH_RSA
|
||||
|| sig_alg == &OID_SIG_DSA_WITH_SHA1
|
||||
|| sig_alg == &OID_PKCS1_SHA256WITHRSA
|
||||
|| sig_alg == &OID_SIG_ECDSA_WITH_SHA256
|
||||
{
|
||||
// ...apart from MD5 or SHA1, which use SHA256 for channel binding, as per RFC 5929 section 4.1:
|
||||
Some(&digest::SHA256)
|
||||
} else if sig_alg == &OID_PKCS1_SHA224WITHRSA || sig_alg == &OID_SIG_ECDSA_WITH_SHA224 {
|
||||
Some(&digest::SHA224)
|
||||
} else if sig_alg == &OID_PKCS1_RSASSAPSS {
|
||||
// For RSASSA-PSS, the hash algorithm is specified in the parameters of the signature algorithm:
|
||||
let params_any = cert.signature_algorithm.parameters()?;
|
||||
let pss = RsaSsaPssParams::try_from(params_any).ok()?;
|
||||
let alg = pss.hash_algorithm_oid();
|
||||
if alg == &OID_NIST_HASH_SHA512 {
|
||||
Some(&digest::SHA512)
|
||||
} else if alg == &OID_NIST_HASH_SHA384 {
|
||||
Some(&digest::SHA384)
|
||||
} else if alg == &OID_NIST_HASH_SHA256 || alg == &OID_HASH_SHA1 {
|
||||
Some(&digest::SHA256)
|
||||
} else if alg == &oid!(2.16.840.1.101.3.4.2.4) {
|
||||
// id-sha224 from RFC 4055 ^
|
||||
Some(&digest::SHA224)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> tokio_postgres::tls::TlsStream for RustlsStream<S>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn channel_binding(&self) -> ChannelBinding {
|
||||
let (_, session) = self.0.get_ref();
|
||||
match session.peer_certificates() {
|
||||
Some(certs) if !certs.is_empty() => match parse_x509_certificate(certs[0].as_ref()) {
|
||||
Ok((_, cert)) => {
|
||||
if let Some(digest_alg) = cb_digest_for_cert(&cert) {
|
||||
let dgst = digest::digest(digest_alg, certs[0].as_ref());
|
||||
ChannelBinding::tls_server_end_point(dgst.as_ref().into())
|
||||
} else {
|
||||
ChannelBinding::none()
|
||||
}
|
||||
}
|
||||
Err(_) => ChannelBinding::none(),
|
||||
},
|
||||
_ => ChannelBinding::none(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> AsyncRead for RustlsStream<S>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<tokio::io::Result<()>> {
|
||||
self.0.as_mut().poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> AsyncWrite for RustlsStream<S>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context,
|
||||
buf: &[u8],
|
||||
) -> Poll<tokio::io::Result<usize>> {
|
||||
self.0.as_mut().poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<tokio::io::Result<()>> {
|
||||
self.0.as_mut().poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<tokio::io::Result<()>> {
|
||||
self.0.as_mut().poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{PostgresStore, into_error, is_timeout_error};
|
||||
use crate::{
|
||||
IndexKey, Key, LogKey, SUBSPACE_COUNTER, SUBSPACE_IN_MEMORY_COUNTER, SUBSPACE_QUOTA,
|
||||
SUBSPACE_REGISTRY_IDX,
|
||||
backend::postgres::{DELETE_CHUNK_SIZE, MIN_DELETE_CHUNK_SIZE, into_pool_error},
|
||||
write::{
|
||||
AssignedIds, Batch, MAX_COMMIT_ATTEMPTS, MAX_COMMIT_TIME, MergeResult, Operation,
|
||||
ValueClass, ValueOp,
|
||||
},
|
||||
};
|
||||
use ahash::AHashMap;
|
||||
use deadpool_postgres::Object;
|
||||
use rand::RngExt;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio_postgres::{IsolationLevel, error::SqlState};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum CommitError {
|
||||
Postgres(tokio_postgres::Error),
|
||||
Internal(trc::Error),
|
||||
//Retry,
|
||||
}
|
||||
|
||||
impl PostgresStore {
|
||||
pub(crate) async fn write(&self, mut batch: Batch<'_>) -> trc::Result<AssignedIds> {
|
||||
let mut conn = self.conn_pool.get().await.map_err(into_pool_error)?;
|
||||
let start = Instant::now();
|
||||
let mut retry_count = 0;
|
||||
|
||||
loop {
|
||||
match self.write_trx(&mut conn, &mut batch).await {
|
||||
Ok(result) => {
|
||||
return Ok(result);
|
||||
}
|
||||
Err(err) => {
|
||||
match err {
|
||||
CommitError::Postgres(err) => match err.code() {
|
||||
Some(
|
||||
&SqlState::T_R_SERIALIZATION_FAILURE
|
||||
| &SqlState::T_R_DEADLOCK_DETECTED,
|
||||
) if retry_count < MAX_COMMIT_ATTEMPTS
|
||||
&& start.elapsed() < MAX_COMMIT_TIME => {}
|
||||
Some(&SqlState::UNIQUE_VIOLATION) => {
|
||||
return Err(trc::StoreEvent::AssertValueFailed
|
||||
.into_err()
|
||||
.reason("Unique violation")
|
||||
.caused_by(trc::location!()));
|
||||
}
|
||||
_ => return Err(into_error(err)),
|
||||
},
|
||||
CommitError::Internal(err) => return Err(err),
|
||||
/*CommitError::Retry => {
|
||||
if retry_count > MAX_COMMIT_ATTEMPTS
|
||||
|| start.elapsed() > MAX_COMMIT_TIME
|
||||
{
|
||||
return Err(trc::StoreEvent::AssertValueFailed
|
||||
.into_err()
|
||||
.caused_by(trc::location!()));
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
let backoff = rand::rng().random_range(50..=300);
|
||||
tokio::time::sleep(Duration::from_millis(backoff)).await;
|
||||
retry_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_trx(
|
||||
&self,
|
||||
conn: &mut Object,
|
||||
batch: &mut Batch<'_>,
|
||||
) -> Result<AssignedIds, CommitError> {
|
||||
let mut account_id = u32::MAX;
|
||||
let mut collection = u8::MAX;
|
||||
let mut document_id = u32::MAX;
|
||||
let mut change_id = 0u64;
|
||||
let mut asserted_values = AHashMap::new();
|
||||
let trx = conn
|
||||
.build_transaction()
|
||||
.isolation_level(IsolationLevel::ReadCommitted)
|
||||
.start()
|
||||
.await?;
|
||||
let mut result = AssignedIds::default();
|
||||
let has_changes = !batch.changes.is_empty();
|
||||
|
||||
if has_changes {
|
||||
for &account_id in batch.changes.keys() {
|
||||
let key = ValueClass::ChangeId.serialize(account_id, 0, 0, 0);
|
||||
let s = trx
|
||||
.prepare_cached(concat!(
|
||||
"INSERT INTO n (k, v) VALUES ($1, 1) ",
|
||||
"ON CONFLICT(k) DO UPDATE SET v = n.v + 1 RETURNING v"
|
||||
))
|
||||
.await?;
|
||||
let change_id = trx
|
||||
.query_one(&s, &[&key])
|
||||
.await
|
||||
.and_then(|row| row.try_get::<_, i64>(0))?;
|
||||
result.push_change_id(account_id, change_id as u64);
|
||||
}
|
||||
}
|
||||
|
||||
for op in batch.ops.iter_mut() {
|
||||
match op {
|
||||
Operation::AccountId {
|
||||
account_id: account_id_,
|
||||
} => {
|
||||
account_id = *account_id_;
|
||||
if has_changes {
|
||||
change_id = result.set_current_change_id(account_id)?;
|
||||
}
|
||||
}
|
||||
Operation::Collection {
|
||||
collection: collection_,
|
||||
} => {
|
||||
collection = u8::from(*collection_);
|
||||
}
|
||||
Operation::DocumentId {
|
||||
document_id: document_id_,
|
||||
} => {
|
||||
document_id = *document_id_;
|
||||
}
|
||||
Operation::Value { class, op } => {
|
||||
let key = class.serialize(account_id, collection, document_id, 0);
|
||||
let subspace = class.subspace(collection);
|
||||
let table = char::from(subspace);
|
||||
|
||||
match op {
|
||||
ValueOp::Set(value) => {
|
||||
if subspace != SUBSPACE_REGISTRY_IDX {
|
||||
let s = if let Some(exists) = asserted_values.get(&key) {
|
||||
if *exists {
|
||||
trx.prepare_cached(&format!(
|
||||
"UPDATE {} SET v = $2 WHERE k = $1",
|
||||
table
|
||||
))
|
||||
.await?
|
||||
} else {
|
||||
trx.prepare_cached(&format!(
|
||||
"INSERT INTO {} (k, v) VALUES ($1, $2)",
|
||||
table
|
||||
))
|
||||
.await?
|
||||
}
|
||||
} else {
|
||||
trx.prepare_cached(&format!(
|
||||
concat!(
|
||||
"INSERT INTO {} (k, v) VALUES ($1, $2) ",
|
||||
"ON CONFLICT (k) DO UPDATE SET v = EXCLUDED.v"
|
||||
),
|
||||
table
|
||||
))
|
||||
.await?
|
||||
};
|
||||
|
||||
if trx.execute(&s, &[&key, &(*value)]).await? == 0 {
|
||||
return Err(trc::StoreEvent::AssertValueFailed
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.into());
|
||||
}
|
||||
} else {
|
||||
let s = trx
|
||||
.prepare_cached(
|
||||
"INSERT INTO b (k) VALUES ($1) ON CONFLICT (k) DO NOTHING",
|
||||
)
|
||||
.await?;
|
||||
trx.execute(&s, &[&key]).await?;
|
||||
}
|
||||
}
|
||||
ValueOp::SetFnc(set_op) => {
|
||||
let value = (set_op.fnc)(&set_op.params, &result)?;
|
||||
|
||||
let s = if let Some(exists) = asserted_values.get(&key) {
|
||||
if *exists {
|
||||
trx.prepare_cached(&format!(
|
||||
"UPDATE {} SET v = $2 WHERE k = $1",
|
||||
table
|
||||
))
|
||||
.await?
|
||||
} else {
|
||||
trx.prepare_cached(&format!(
|
||||
"INSERT INTO {} (k, v) VALUES ($1, $2)",
|
||||
table
|
||||
))
|
||||
.await?
|
||||
}
|
||||
} else {
|
||||
trx.prepare_cached(&format!(
|
||||
concat!(
|
||||
"INSERT INTO {} (k, v) VALUES ($1, $2) ",
|
||||
"ON CONFLICT (k) DO UPDATE SET v = EXCLUDED.v"
|
||||
),
|
||||
table
|
||||
))
|
||||
.await?
|
||||
};
|
||||
|
||||
if trx.execute(&s, &[&key, &value]).await? == 0 {
|
||||
return Err(trc::StoreEvent::AssertValueFailed
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.into());
|
||||
}
|
||||
}
|
||||
ValueOp::MergeFnc(merge_op) => {
|
||||
let s = trx
|
||||
.prepare_cached(&format!(
|
||||
"SELECT v FROM {} WHERE k = $1 FOR UPDATE",
|
||||
table
|
||||
))
|
||||
.await?;
|
||||
let (exists, merge_result) = trx
|
||||
.query_opt(&s, &[&key])
|
||||
.await?
|
||||
.map(|row| {
|
||||
row.try_get::<_, &[u8]>(0)
|
||||
.map_err(CommitError::from)
|
||||
.and_then(|v| {
|
||||
(merge_op.fnc)(&merge_op.params, &result, Some(v))
|
||||
.map(|v| (true, v))
|
||||
.map_err(CommitError::from)
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
(merge_op.fnc)(&merge_op.params, &result, None)
|
||||
.map(|v| (false, v))
|
||||
.map_err(CommitError::from)
|
||||
})?;
|
||||
|
||||
match merge_result {
|
||||
MergeResult::Update(value) => {
|
||||
let s = if exists {
|
||||
trx.prepare_cached(&format!(
|
||||
"UPDATE {} SET v = $2 WHERE k = $1",
|
||||
table
|
||||
))
|
||||
.await?
|
||||
} else {
|
||||
trx.prepare_cached(&format!(
|
||||
"INSERT INTO {} (k, v) VALUES ($1, $2)",
|
||||
table
|
||||
))
|
||||
.await?
|
||||
};
|
||||
|
||||
trx.execute(&s, &[&key, &value]).await?;
|
||||
}
|
||||
MergeResult::Delete if exists => {
|
||||
let s = trx
|
||||
.prepare_cached(&format!(
|
||||
"DELETE FROM {} WHERE k = $1",
|
||||
table
|
||||
))
|
||||
.await?;
|
||||
trx.execute(&s, &[&key]).await?;
|
||||
|
||||
// Update asserted value
|
||||
if let Some(exists) = asserted_values.get_mut(&key) {
|
||||
*exists = false;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
ValueOp::AtomicAdd(by) => {
|
||||
if *by >= 0 {
|
||||
let s = trx
|
||||
.prepare_cached(&format!(
|
||||
concat!(
|
||||
"INSERT INTO {} (k, v) VALUES ($1, $2) ",
|
||||
"ON CONFLICT(k) DO UPDATE SET v = {}.v + EXCLUDED.v"
|
||||
),
|
||||
table, table
|
||||
))
|
||||
.await?;
|
||||
trx.execute(&s, &[&key, &*by]).await?;
|
||||
} else {
|
||||
let s = trx
|
||||
.prepare_cached(&format!(
|
||||
"UPDATE {table} SET v = v + $1 WHERE k = $2"
|
||||
))
|
||||
.await?;
|
||||
trx.execute(&s, &[&*by, &key]).await?;
|
||||
}
|
||||
}
|
||||
ValueOp::AddAndGet(by) => {
|
||||
let s = trx
|
||||
.prepare_cached(&format!(
|
||||
concat!(
|
||||
"INSERT INTO {} (k, v) VALUES ($1, $2) ",
|
||||
"ON CONFLICT(k) DO UPDATE SET v = {}.v + EXCLUDED.v RETURNING v"
|
||||
),
|
||||
table, table
|
||||
))
|
||||
.await?;
|
||||
result.push_counter_id(
|
||||
trx.query_one(&s, &[&key, &*by])
|
||||
.await
|
||||
.and_then(|row| row.try_get::<_, i64>(0))?,
|
||||
);
|
||||
}
|
||||
ValueOp::Clear => {
|
||||
let s = trx
|
||||
.prepare_cached(&format!("DELETE FROM {} WHERE k = $1", table))
|
||||
.await?;
|
||||
trx.execute(&s, &[&key]).await?;
|
||||
|
||||
// Update asserted value
|
||||
if let Some(exists) = asserted_values.get_mut(&key) {
|
||||
*exists = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Operation::Index { field, key, set } => {
|
||||
let key = IndexKey {
|
||||
account_id,
|
||||
collection,
|
||||
document_id,
|
||||
field: *field,
|
||||
key: &*key,
|
||||
}
|
||||
.serialize(0);
|
||||
|
||||
let s = if *set {
|
||||
trx.prepare_cached(
|
||||
"INSERT INTO i (k) VALUES ($1) ON CONFLICT (k) DO NOTHING",
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
trx.prepare_cached("DELETE FROM i WHERE k = $1").await?
|
||||
};
|
||||
trx.execute(&s, &[&key]).await?;
|
||||
}
|
||||
Operation::Log { collection, set } => {
|
||||
let key = LogKey {
|
||||
account_id,
|
||||
collection: u8::from(*collection),
|
||||
change_id,
|
||||
}
|
||||
.serialize(0);
|
||||
|
||||
let s = trx
|
||||
.prepare_cached(concat!(
|
||||
"INSERT INTO l (k, v) VALUES ($1, $2) ",
|
||||
"ON CONFLICT (k) DO UPDATE SET v = EXCLUDED.v"
|
||||
))
|
||||
.await?;
|
||||
|
||||
trx.execute(&s, &[&key, &*set]).await?;
|
||||
}
|
||||
Operation::AssertValue {
|
||||
class,
|
||||
assert_value,
|
||||
} => {
|
||||
let key = class.serialize(account_id, collection, document_id, 0);
|
||||
let table = char::from(class.subspace(collection));
|
||||
|
||||
let s = trx
|
||||
.prepare_cached(&format!("SELECT v FROM {} WHERE k = $1 FOR UPDATE", table))
|
||||
.await?;
|
||||
let (exists, matches) = trx
|
||||
.query_opt(&s, &[&key])
|
||||
.await?
|
||||
.map(|row| {
|
||||
row.try_get::<_, &[u8]>(0)
|
||||
.map_or((true, false), |v| (true, assert_value.matches(v)))
|
||||
})
|
||||
.unwrap_or_else(|| (false, assert_value.is_none()));
|
||||
if !matches {
|
||||
return Err(trc::StoreEvent::AssertValueFailed
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.into());
|
||||
}
|
||||
asserted_values.insert(key, exists);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trx.commit().await.map(|_| result).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) async fn purge_store(&self) -> trc::Result<()> {
|
||||
let conn = self.conn_pool.get().await.map_err(into_pool_error)?;
|
||||
|
||||
for subspace in [SUBSPACE_QUOTA, SUBSPACE_COUNTER, SUBSPACE_IN_MEMORY_COUNTER] {
|
||||
purge_table(&conn, char::from(subspace)).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_range(&self, from: impl Key, to: impl Key) -> trc::Result<()> {
|
||||
let conn = self.conn_pool.get().await.map_err(into_pool_error)?;
|
||||
let table = char::from(from.subspace());
|
||||
let mut from = from.serialize(0);
|
||||
let to = to.serialize(0);
|
||||
|
||||
let delete = conn
|
||||
.prepare_cached(&format!("DELETE FROM {table} WHERE k >= $1 AND k < $2"))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
|
||||
match conn.execute(&delete, &[&from, &to]).await {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(err) if is_timeout_error(&err) => (),
|
||||
Err(err) => return Err(into_error(err)),
|
||||
}
|
||||
|
||||
let mut chunk_size = DELETE_CHUNK_SIZE;
|
||||
|
||||
loop {
|
||||
let boundary = conn
|
||||
.prepare_cached(&format!(
|
||||
"SELECT k FROM {table} WHERE k >= $1 AND k < $2 ORDER BY k ASC LIMIT 1 OFFSET {chunk_size}"
|
||||
))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
|
||||
loop {
|
||||
let next = match conn.query_opt(&boundary, &[&from, &to]).await {
|
||||
Ok(next) => match next {
|
||||
Some(row) => Some(row.try_get::<_, Vec<u8>>(0).map_err(into_error)?),
|
||||
None => None,
|
||||
},
|
||||
Err(err) if is_timeout_error(&err) && chunk_size > MIN_DELETE_CHUNK_SIZE => {
|
||||
chunk_size = (chunk_size / 2).max(MIN_DELETE_CHUNK_SIZE);
|
||||
break;
|
||||
}
|
||||
Err(err) => return Err(into_error(err)),
|
||||
};
|
||||
|
||||
match conn
|
||||
.execute(&delete, &[&from, next.as_ref().unwrap_or(&to)])
|
||||
.await
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(err) if is_timeout_error(&err) && chunk_size > MIN_DELETE_CHUNK_SIZE => {
|
||||
chunk_size = (chunk_size / 2).max(MIN_DELETE_CHUNK_SIZE);
|
||||
break;
|
||||
}
|
||||
Err(err) => return Err(into_error(err)),
|
||||
}
|
||||
|
||||
match next {
|
||||
Some(next) => from = next,
|
||||
None => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn purge_table(conn: &Object, table: char) -> trc::Result<()> {
|
||||
let s = conn
|
||||
.prepare_cached(&format!("DELETE FROM {table} WHERE v = 0"))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
|
||||
match conn.execute(&s, &[]).await {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(err) if is_timeout_error(&err) => (),
|
||||
Err(err) => return Err(into_error(err)),
|
||||
}
|
||||
|
||||
let purge = conn
|
||||
.prepare_cached(&format!(
|
||||
"DELETE FROM {table} WHERE v = 0 AND k >= $1 AND k < $2"
|
||||
))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
let purge_last = conn
|
||||
.prepare_cached(&format!("DELETE FROM {table} WHERE v = 0 AND k >= $1"))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
let mut chunk_size = DELETE_CHUNK_SIZE;
|
||||
let mut from = Vec::new();
|
||||
|
||||
loop {
|
||||
let boundary = conn
|
||||
.prepare_cached(&format!(
|
||||
"SELECT k FROM {table} WHERE k >= $1 ORDER BY k ASC LIMIT 1 OFFSET {chunk_size}"
|
||||
))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
|
||||
loop {
|
||||
let next = match conn.query_opt(&boundary, &[&from]).await {
|
||||
Ok(next) => match next {
|
||||
Some(row) => Some(row.try_get::<_, Vec<u8>>(0).map_err(into_error)?),
|
||||
None => None,
|
||||
},
|
||||
Err(err) if is_timeout_error(&err) && chunk_size > MIN_DELETE_CHUNK_SIZE => {
|
||||
chunk_size = (chunk_size / 2).max(MIN_DELETE_CHUNK_SIZE);
|
||||
break;
|
||||
}
|
||||
Err(err) => return Err(into_error(err)),
|
||||
};
|
||||
|
||||
let result = match &next {
|
||||
Some(next) => conn.execute(&purge, &[&from, next]).await,
|
||||
None => conn.execute(&purge_last, &[&from]).await,
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(_) => (),
|
||||
Err(err) if is_timeout_error(&err) && chunk_size > MIN_DELETE_CHUNK_SIZE => {
|
||||
chunk_size = (chunk_size / 2).max(MIN_DELETE_CHUNK_SIZE);
|
||||
break;
|
||||
}
|
||||
Err(err) => return Err(into_error(err)),
|
||||
}
|
||||
|
||||
match next {
|
||||
Some(next) => from = next,
|
||||
None => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<trc::Error> for CommitError {
|
||||
fn from(err: trc::Error) -> Self {
|
||||
CommitError::Internal(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tokio_postgres::Error> for CommitError {
|
||||
fn from(err: tokio_postgres::Error) -> Self {
|
||||
CommitError::Postgres(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{RedisPool, RedisStore, into_error};
|
||||
use crate::{Deserialize, write::now};
|
||||
use redis::AsyncCommands;
|
||||
|
||||
impl RedisStore {
|
||||
pub async fn key_set(&self, key: &[u8], value: &[u8], expires: Option<u64>) -> trc::Result<()> {
|
||||
match &self.pool {
|
||||
RedisPool::Single(pool) => {
|
||||
self.key_set_(
|
||||
pool.get().await.map_err(into_error)?.as_mut(),
|
||||
key,
|
||||
value,
|
||||
expires,
|
||||
)
|
||||
.await
|
||||
}
|
||||
RedisPool::Cluster(pool) => {
|
||||
self.key_set_(
|
||||
pool.get().await.map_err(into_error)?.as_mut(),
|
||||
key,
|
||||
value,
|
||||
expires,
|
||||
)
|
||||
.await
|
||||
}
|
||||
RedisPool::Sentinel(pool) => {
|
||||
self.key_set_(
|
||||
pool.get().await.map_err(into_error)?.as_mut(),
|
||||
key,
|
||||
value,
|
||||
expires,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn key_incr(&self, key: &[u8], value: i64, expires: Option<u64>) -> trc::Result<i64> {
|
||||
match &self.pool {
|
||||
RedisPool::Single(pool) => {
|
||||
self.key_incr_(
|
||||
pool.get().await.map_err(into_error)?.as_mut(),
|
||||
key,
|
||||
value,
|
||||
expires,
|
||||
)
|
||||
.await
|
||||
}
|
||||
RedisPool::Cluster(pool) => {
|
||||
self.key_incr_(
|
||||
pool.get().await.map_err(into_error)?.as_mut(),
|
||||
key,
|
||||
value,
|
||||
expires,
|
||||
)
|
||||
.await
|
||||
}
|
||||
RedisPool::Sentinel(pool) => {
|
||||
self.key_incr_(
|
||||
pool.get().await.map_err(into_error)?.as_mut(),
|
||||
key,
|
||||
value,
|
||||
expires,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn try_lock(&self, key: &[u8], expires: u64) -> trc::Result<bool> {
|
||||
match &self.pool {
|
||||
RedisPool::Single(pool) => {
|
||||
self.try_lock_(pool.get().await.map_err(into_error)?.as_mut(), key, expires)
|
||||
.await
|
||||
}
|
||||
RedisPool::Cluster(pool) => {
|
||||
self.try_lock_(pool.get().await.map_err(into_error)?.as_mut(), key, expires)
|
||||
.await
|
||||
}
|
||||
RedisPool::Sentinel(pool) => {
|
||||
self.try_lock_(pool.get().await.map_err(into_error)?.as_mut(), key, expires)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn key_delete(&self, key: &[u8]) -> trc::Result<()> {
|
||||
match &self.pool {
|
||||
RedisPool::Single(pool) => {
|
||||
self.key_delete_(pool.get().await.map_err(into_error)?.as_mut(), key)
|
||||
.await
|
||||
}
|
||||
RedisPool::Cluster(pool) => {
|
||||
self.key_delete_(pool.get().await.map_err(into_error)?.as_mut(), key)
|
||||
.await
|
||||
}
|
||||
RedisPool::Sentinel(pool) => {
|
||||
self.key_delete_(pool.get().await.map_err(into_error)?.as_mut(), key)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn key_delete_prefix(&self, prefix: &[u8]) -> trc::Result<()> {
|
||||
match &self.pool {
|
||||
RedisPool::Single(pool) => {
|
||||
self.key_delete_prefix_(pool.get().await.map_err(into_error)?.as_mut(), prefix)
|
||||
.await
|
||||
}
|
||||
RedisPool::Cluster(pool) => {
|
||||
self.key_delete_prefix_(pool.get().await.map_err(into_error)?.as_mut(), prefix)
|
||||
.await
|
||||
}
|
||||
RedisPool::Sentinel(pool) => {
|
||||
self.key_delete_prefix_(pool.get().await.map_err(into_error)?.as_mut(), prefix)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn key_get<T: Deserialize + std::fmt::Debug + 'static>(
|
||||
&self,
|
||||
key: &[u8],
|
||||
) -> trc::Result<Option<T>> {
|
||||
match &self.pool {
|
||||
RedisPool::Single(pool) => {
|
||||
self.key_get_(pool.get().await.map_err(into_error)?.as_mut(), key)
|
||||
.await
|
||||
}
|
||||
RedisPool::Cluster(pool) => {
|
||||
self.key_get_(pool.get().await.map_err(into_error)?.as_mut(), key)
|
||||
.await
|
||||
}
|
||||
RedisPool::Sentinel(pool) => {
|
||||
self.key_get_(pool.get().await.map_err(into_error)?.as_mut(), key)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn counter_get(&self, key: &[u8]) -> trc::Result<i64> {
|
||||
match &self.pool {
|
||||
RedisPool::Single(pool) => {
|
||||
self.counter_get_(pool.get().await.map_err(into_error)?.as_mut(), key)
|
||||
.await
|
||||
}
|
||||
RedisPool::Cluster(pool) => {
|
||||
self.counter_get_(pool.get().await.map_err(into_error)?.as_mut(), key)
|
||||
.await
|
||||
}
|
||||
RedisPool::Sentinel(pool) => {
|
||||
self.counter_get_(pool.get().await.map_err(into_error)?.as_mut(), key)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn key_exists(&self, key: &[u8]) -> trc::Result<bool> {
|
||||
match &self.pool {
|
||||
RedisPool::Single(pool) => {
|
||||
self.key_exists_(pool.get().await.map_err(into_error)?.as_mut(), key)
|
||||
.await
|
||||
}
|
||||
RedisPool::Cluster(pool) => {
|
||||
self.key_exists_(pool.get().await.map_err(into_error)?.as_mut(), key)
|
||||
.await
|
||||
}
|
||||
RedisPool::Sentinel(pool) => {
|
||||
self.key_exists_(pool.get().await.map_err(into_error)?.as_mut(), key)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn key_get_<T: Deserialize + std::fmt::Debug + 'static>(
|
||||
&self,
|
||||
conn: &mut impl AsyncCommands,
|
||||
key: &[u8],
|
||||
) -> trc::Result<Option<T>> {
|
||||
if let Some(value) = redis::cmd("GET")
|
||||
.arg(key)
|
||||
.query_async::<Option<Vec<u8>>>(conn)
|
||||
.await
|
||||
.map_err(into_error)?
|
||||
{
|
||||
T::deserialize_owned(value).map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn counter_get_(&self, conn: &mut impl AsyncCommands, key: &[u8]) -> trc::Result<i64> {
|
||||
redis::cmd("GET")
|
||||
.arg(key)
|
||||
.query_async::<Option<i64>>(conn)
|
||||
.await
|
||||
.map(|x| x.unwrap_or(0))
|
||||
.map_err(into_error)
|
||||
}
|
||||
|
||||
async fn key_exists_(&self, conn: &mut impl AsyncCommands, key: &[u8]) -> trc::Result<bool> {
|
||||
conn.exists(key).await.map_err(into_error)
|
||||
}
|
||||
|
||||
async fn key_set_(
|
||||
&self,
|
||||
conn: &mut impl AsyncCommands,
|
||||
key: &[u8],
|
||||
value: &[u8],
|
||||
expires: Option<u64>,
|
||||
) -> trc::Result<()> {
|
||||
if let Some(expires) = expires {
|
||||
conn.set_ex(key, value, expires).await.map_err(into_error)
|
||||
} else {
|
||||
conn.set(key, value).await.map_err(into_error)
|
||||
}
|
||||
}
|
||||
|
||||
async fn key_incr_(
|
||||
&self,
|
||||
conn: &mut impl AsyncCommands,
|
||||
key: &[u8],
|
||||
value: i64,
|
||||
expires: Option<u64>,
|
||||
) -> trc::Result<i64> {
|
||||
if let Some(expires) = expires {
|
||||
redis::pipe()
|
||||
.atomic()
|
||||
.incr(key, value)
|
||||
.expire(key, expires as i64)
|
||||
.ignore()
|
||||
.query_async::<Vec<i64>>(conn)
|
||||
.await
|
||||
.map_err(into_error)
|
||||
.map(|v| v.first().copied().unwrap_or(0))
|
||||
} else {
|
||||
conn.incr(key, value).await.map_err(into_error)
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_lock_(
|
||||
&self,
|
||||
conn: &mut impl AsyncCommands,
|
||||
key: &[u8],
|
||||
expires: u64,
|
||||
) -> trc::Result<bool> {
|
||||
redis::cmd("SET")
|
||||
.arg(key)
|
||||
.arg(now() + expires)
|
||||
.arg("NX")
|
||||
.arg("EX")
|
||||
.arg(expires as i64)
|
||||
.query_async::<Option<String>>(conn)
|
||||
.await
|
||||
.map(|reply| reply.is_some())
|
||||
.map_err(into_error)
|
||||
}
|
||||
|
||||
async fn key_delete_(&self, conn: &mut impl AsyncCommands, key: &[u8]) -> trc::Result<()> {
|
||||
conn.del(key).await.map_err(into_error)
|
||||
}
|
||||
|
||||
async fn key_delete_prefix_(
|
||||
&self,
|
||||
conn: &mut impl AsyncCommands,
|
||||
prefix: &[u8],
|
||||
) -> trc::Result<()> {
|
||||
let mut pattern = Vec::with_capacity(prefix.len() + 1);
|
||||
pattern.extend_from_slice(prefix);
|
||||
pattern.push(b'*');
|
||||
|
||||
let mut cursor = 0;
|
||||
loop {
|
||||
let (new_cursor, keys): (u64, Vec<Vec<u8>>) = redis::cmd("SCAN")
|
||||
.cursor_arg(cursor)
|
||||
.arg("MATCH")
|
||||
.arg(&pattern)
|
||||
.arg("COUNT")
|
||||
.arg(100)
|
||||
.query_async(conn)
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
|
||||
if !keys.is_empty() {
|
||||
conn.del::<_, ()>(&keys).await.map_err(into_error)?;
|
||||
}
|
||||
|
||||
if new_cursor != 0 {
|
||||
cursor = new_cursor;
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::InMemoryStore;
|
||||
use deadpool::{
|
||||
Runtime,
|
||||
managed::{Manager, Pool},
|
||||
};
|
||||
use redis::{
|
||||
Client, ConnectionAddr, IntoConnectionInfo, ProtocolVersion, TlsMode,
|
||||
cluster::{ClusterClient, ClusterClientBuilder},
|
||||
cluster_read_routing::RandomReplicaStrategy,
|
||||
sentinel::{SentinelClient, SentinelClientBuilder, SentinelServerType},
|
||||
};
|
||||
use registry::{
|
||||
schema::{enums::RedisProtocol, structs},
|
||||
types::duration::Duration,
|
||||
};
|
||||
use std::{fmt::Display, sync::Arc};
|
||||
|
||||
pub mod lookup;
|
||||
pub mod pool;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RedisStore {
|
||||
pub pool: RedisPool,
|
||||
}
|
||||
|
||||
pub struct RedisConnectionManager {
|
||||
pub client: Client,
|
||||
timeout: std::time::Duration,
|
||||
}
|
||||
|
||||
pub struct RedisClusterConnectionManager {
|
||||
pub client: ClusterClient,
|
||||
timeout: std::time::Duration,
|
||||
}
|
||||
|
||||
pub struct RedisSentinelConnectionManager {
|
||||
pub client: tokio::sync::Mutex<SentinelClient>,
|
||||
timeout: std::time::Duration,
|
||||
}
|
||||
|
||||
pub enum RedisPool {
|
||||
Single(Pool<RedisConnectionManager>),
|
||||
Cluster(Pool<RedisClusterConnectionManager>),
|
||||
Sentinel(Pool<RedisSentinelConnectionManager>),
|
||||
}
|
||||
|
||||
impl RedisStore {
|
||||
pub async fn open_single(config: structs::RedisStore) -> Result<InMemoryStore, String> {
|
||||
Ok(InMemoryStore::Redis(Arc::new(RedisStore {
|
||||
pool: RedisPool::Single(build_pool(
|
||||
RedisConnectionManager {
|
||||
client: Client::open(config.url)
|
||||
.map_err(|err| format!("Failed to open Redis client: {err:?}"))?,
|
||||
timeout: config.timeout.into_inner(),
|
||||
},
|
||||
config.pool_max_connections,
|
||||
config.pool_timeout_create,
|
||||
config.pool_timeout_wait,
|
||||
config.pool_timeout_recycle,
|
||||
)?),
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn open_cluster(config: structs::RedisClusterStore) -> Result<InMemoryStore, String> {
|
||||
let mut builder = ClusterClientBuilder::new(config.urls);
|
||||
if let Some(value) = config.auth_username {
|
||||
builder = builder.username(value);
|
||||
}
|
||||
if let Some(value) = config.auth_secret.secret().await?.map(|v| v.into_owned()) {
|
||||
builder = builder.password(value);
|
||||
}
|
||||
if let Some(value) = config.max_retries {
|
||||
builder = builder.retries(value as u32);
|
||||
}
|
||||
if let Some(value) = config.max_retry_wait {
|
||||
builder = builder.max_retry_wait(value.as_millis());
|
||||
}
|
||||
if let Some(value) = config.min_retry_wait {
|
||||
builder = builder.min_retry_wait(value.as_millis());
|
||||
}
|
||||
if config.read_from_replicas {
|
||||
builder = builder.read_routing_strategy(RandomReplicaStrategy);
|
||||
}
|
||||
if matches!(config.protocol_version, RedisProtocol::Resp3) {
|
||||
builder = builder.use_protocol(ProtocolVersion::RESP3);
|
||||
}
|
||||
|
||||
let client = builder
|
||||
.build()
|
||||
.map_err(|err| format!("Failed to open Redis client: {err:?}"))?;
|
||||
|
||||
Ok(InMemoryStore::Redis(Arc::new(RedisStore {
|
||||
pool: RedisPool::Cluster(build_pool(
|
||||
RedisClusterConnectionManager {
|
||||
client,
|
||||
timeout: config.timeout.into_inner(),
|
||||
},
|
||||
config.pool_max_connections,
|
||||
config.pool_timeout_create,
|
||||
config.pool_timeout_wait,
|
||||
config.pool_timeout_recycle,
|
||||
)?),
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn open_sentinel(
|
||||
config: structs::RedisSentinelStore,
|
||||
) -> Result<InMemoryStore, String> {
|
||||
let mut sentinels = Vec::with_capacity(config.urls.len());
|
||||
let mut tls_mode = None;
|
||||
for url in config.urls {
|
||||
let info = url
|
||||
.into_connection_info()
|
||||
.map_err(|err| format!("Invalid Redis Sentinel URL: {err}"))?;
|
||||
let url_tls_mode = match info.addr() {
|
||||
ConnectionAddr::TcpTls { insecure: true, .. } => Some(TlsMode::Insecure),
|
||||
ConnectionAddr::TcpTls {
|
||||
insecure: false, ..
|
||||
} => Some(TlsMode::Secure),
|
||||
_ => None,
|
||||
};
|
||||
if sentinels.is_empty() {
|
||||
tls_mode = url_tls_mode;
|
||||
} else if tls_mode != url_tls_mode {
|
||||
return Err(
|
||||
"All Redis Sentinel URLs must use the same scheme and TLS settings".to_string(),
|
||||
);
|
||||
}
|
||||
sentinels.push(info.addr().clone());
|
||||
}
|
||||
|
||||
let mut builder =
|
||||
SentinelClientBuilder::new(sentinels, config.service_name, SentinelServerType::Master)
|
||||
.map_err(|err| format!("Failed to create Redis Sentinel client: {err:?}"))?;
|
||||
|
||||
if let Some(value) = config.auth_username {
|
||||
builder = builder.set_client_to_redis_username(value);
|
||||
}
|
||||
if let Some(value) = config.auth_secret.secret().await?.map(|v| v.into_owned()) {
|
||||
builder = builder.set_client_to_redis_password(value);
|
||||
}
|
||||
if let Some(value) = config.sentinel_username {
|
||||
builder = builder.set_client_to_sentinel_username(value);
|
||||
}
|
||||
if let Some(value) = config
|
||||
.sentinel_secret
|
||||
.secret()
|
||||
.await?
|
||||
.map(|v| v.into_owned())
|
||||
{
|
||||
builder = builder.set_client_to_sentinel_password(value);
|
||||
}
|
||||
if matches!(config.protocol_version, RedisProtocol::Resp3) {
|
||||
builder = builder.set_client_to_redis_protocol(ProtocolVersion::RESP3);
|
||||
}
|
||||
if let Some(tls_mode) = tls_mode {
|
||||
builder = builder.set_client_to_redis_tls_mode(tls_mode);
|
||||
}
|
||||
|
||||
let client = builder
|
||||
.build()
|
||||
.map_err(|err| format!("Failed to open Redis Sentinel client: {err:?}"))?;
|
||||
|
||||
Ok(InMemoryStore::Redis(Arc::new(RedisStore {
|
||||
pool: RedisPool::Sentinel(build_pool(
|
||||
RedisSentinelConnectionManager {
|
||||
client: tokio::sync::Mutex::new(client),
|
||||
timeout: config.timeout.into_inner(),
|
||||
},
|
||||
config.pool_max_connections,
|
||||
config.pool_timeout_create,
|
||||
config.pool_timeout_wait,
|
||||
config.pool_timeout_recycle,
|
||||
)?),
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
fn build_pool<M: Manager>(
|
||||
manager: M,
|
||||
max_size: u64,
|
||||
create_timeout: Option<Duration>,
|
||||
wait_timeout: Option<Duration>,
|
||||
recycle_timeout: Option<Duration>,
|
||||
) -> Result<Pool<M>, String> {
|
||||
Pool::builder(manager)
|
||||
.runtime(Runtime::Tokio1)
|
||||
.max_size(max_size as usize)
|
||||
.create_timeout(create_timeout.map(|v| v.into_inner()))
|
||||
.wait_timeout(wait_timeout.map(|v| v.into_inner()))
|
||||
.recycle_timeout(recycle_timeout.map(|v| v.into_inner()))
|
||||
.build()
|
||||
.map_err(|err| format!("Failed to build pool: {err}"))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn into_error(err: impl Display) -> trc::Error {
|
||||
trc::StoreEvent::RedisError.reason(err)
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RedisPool {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Single(_) => f.debug_tuple("Single").finish(),
|
||||
Self::Cluster(_) => f.debug_tuple("Cluster").finish(),
|
||||
Self::Sentinel(_) => f.debug_tuple("Sentinel").finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{
|
||||
RedisClusterConnectionManager, RedisConnectionManager, RedisSentinelConnectionManager,
|
||||
into_error,
|
||||
};
|
||||
use deadpool::managed;
|
||||
use redis::{
|
||||
aio::{ConnectionLike, MultiplexedConnection},
|
||||
cluster_async::ClusterConnection,
|
||||
};
|
||||
|
||||
impl managed::Manager for RedisConnectionManager {
|
||||
type Type = MultiplexedConnection;
|
||||
type Error = trc::Error;
|
||||
|
||||
async fn create(&self) -> Result<MultiplexedConnection, trc::Error> {
|
||||
match tokio::time::timeout(self.timeout, self.client.get_multiplexed_async_connection())
|
||||
.await
|
||||
{
|
||||
Ok(conn) => conn.map_err(into_error),
|
||||
Err(_) => Err(trc::StoreEvent::RedisError.ctx(trc::Key::Details, "Connection Timeout")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn recycle(
|
||||
&self,
|
||||
conn: &mut MultiplexedConnection,
|
||||
_: &managed::Metrics,
|
||||
) -> managed::RecycleResult<trc::Error> {
|
||||
conn.req_packed_command(&redis::cmd("PING"))
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|err| managed::RecycleError::Backend(into_error(err)))
|
||||
}
|
||||
}
|
||||
|
||||
impl managed::Manager for RedisClusterConnectionManager {
|
||||
type Type = ClusterConnection;
|
||||
type Error = trc::Error;
|
||||
|
||||
async fn create(&self) -> Result<ClusterConnection, trc::Error> {
|
||||
match tokio::time::timeout(self.timeout, self.client.get_async_connection()).await {
|
||||
Ok(conn) => conn.map_err(into_error),
|
||||
Err(_) => Err(trc::StoreEvent::RedisError.ctx(trc::Key::Details, "Connection Timeout")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn recycle(
|
||||
&self,
|
||||
conn: &mut ClusterConnection,
|
||||
_: &managed::Metrics,
|
||||
) -> managed::RecycleResult<trc::Error> {
|
||||
conn.req_packed_command(&redis::cmd("PING"))
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|err| managed::RecycleError::Backend(into_error(err)))
|
||||
}
|
||||
}
|
||||
|
||||
impl managed::Manager for RedisSentinelConnectionManager {
|
||||
type Type = MultiplexedConnection;
|
||||
type Error = trc::Error;
|
||||
|
||||
async fn create(&self) -> Result<MultiplexedConnection, trc::Error> {
|
||||
let mut client = self.client.lock().await;
|
||||
match tokio::time::timeout(self.timeout, client.get_async_connection()).await {
|
||||
Ok(conn) => conn.map_err(into_error),
|
||||
Err(_) => Err(trc::StoreEvent::RedisError.ctx(trc::Key::Details, "Connection Timeout")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn recycle(
|
||||
&self,
|
||||
conn: &mut MultiplexedConnection,
|
||||
_: &managed::Metrics,
|
||||
) -> managed::RecycleResult<trc::Error> {
|
||||
conn.req_packed_command(&redis::cmd("PING"))
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|err| managed::RecycleError::Backend(into_error(err)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use super::{CF_BLOBS, RocksDbStore, into_error};
|
||||
|
||||
impl RocksDbStore {
|
||||
pub(crate) async fn get_blob(
|
||||
&self,
|
||||
key: &[u8],
|
||||
range: Range<usize>,
|
||||
) -> trc::Result<Option<Vec<u8>>> {
|
||||
let db = self.db.clone();
|
||||
self.spawn_worker(move || {
|
||||
db.get_pinned_cf(&db.cf_handle(CF_BLOBS).unwrap(), key)
|
||||
.map(|obj| {
|
||||
obj.map(|bytes| {
|
||||
if range.start == 0 && range.end == usize::MAX {
|
||||
bytes.to_vec()
|
||||
} else {
|
||||
bytes
|
||||
.get(range.start..std::cmp::min(bytes.len(), range.end))
|
||||
.unwrap_or_default()
|
||||
.to_vec()
|
||||
}
|
||||
})
|
||||
})
|
||||
.map_err(into_error)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn put_blob(&self, key: &[u8], data: &[u8]) -> trc::Result<()> {
|
||||
let db = self.db.clone();
|
||||
self.spawn_worker(move || {
|
||||
db.put_cf(&db.cf_handle(CF_BLOBS).unwrap(), key, data)
|
||||
.map_err(into_error)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_blob(&self, key: &[u8]) -> trc::Result<bool> {
|
||||
let db = self.db.clone();
|
||||
self.spawn_worker(move || {
|
||||
db.delete_cf(&db.cf_handle(CF_BLOBS).unwrap(), key)
|
||||
.map_err(into_error)
|
||||
.map(|_| true)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{CF_BLOBS, RocksDbStore};
|
||||
use crate::*;
|
||||
use ::registry::schema::structs;
|
||||
use rocksdb::{
|
||||
BlockBasedOptions, Cache, ColumnFamilyDescriptor, DBCompressionType, MergeOperands,
|
||||
OptimisticTransactionDB, Options,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
const MIN_WRITE_BUFFER_SIZE: usize = 4 * 1024 * 1024;
|
||||
const MAX_WRITE_BUFFER_SIZE: usize = 64 * 1024 * 1024;
|
||||
const MIN_DB_WRITE_BUFFER_SIZE: usize = 32 * 1024 * 1024;
|
||||
const BLOOM_BITS_PER_KEY: f64 = 10.0;
|
||||
const SCAN_BLOCK_SIZE: usize = 16 * 1024;
|
||||
const CHURN_TARGET_FILE_SIZE: u64 = 16 * 1024 * 1024;
|
||||
const CHURN_DELETION_WINDOW: usize = 4096;
|
||||
const CHURN_DELETION_TRIGGER: usize = 1024;
|
||||
const CHURN_DELETION_RATIO: f64 = 0.5;
|
||||
const BYTES_PER_SYNC: u64 = 1024 * 1024;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum CfProfile {
|
||||
/// Read through `get_value` / `key_exists`, so a whole key bloom filter pays off.
|
||||
PointLookup,
|
||||
/// Read only through `iterate`, which never consults a whole key bloom filter.
|
||||
Scan,
|
||||
/// Point read and point deleted at a high rate.
|
||||
Churn,
|
||||
/// Scanned from the oldest key and point deleted once consumed, with empty values.
|
||||
Queue,
|
||||
/// Counters updated through the merge operator.
|
||||
Counter,
|
||||
/// Blob values held in RocksDB blob files.
|
||||
Blob,
|
||||
}
|
||||
|
||||
impl RocksDbStore {
|
||||
pub async fn open(config: structs::RocksDbStore) -> Result<Store, String> {
|
||||
// Create the database directory if it doesn't exist
|
||||
let idx_path: PathBuf = PathBuf::from(config.path);
|
||||
std::fs::create_dir_all(&idx_path).map_err(|err| {
|
||||
format!(
|
||||
"Failed to create database directory {}: {:?}",
|
||||
idx_path.display(),
|
||||
err
|
||||
)
|
||||
})?;
|
||||
|
||||
let cache = Cache::new_lru_cache(config.cache_size as usize);
|
||||
let write_buffer_size =
|
||||
((config.buffer_size as usize) / 4).clamp(MIN_WRITE_BUFFER_SIZE, MAX_WRITE_BUFFER_SIZE);
|
||||
let mut cfs = Vec::new();
|
||||
|
||||
// Counters
|
||||
for subspace in [SUBSPACE_COUNTER, SUBSPACE_QUOTA, SUBSPACE_IN_MEMORY_COUNTER] {
|
||||
cfs.push(ColumnFamilyDescriptor::new(
|
||||
std::str::from_utf8(&[subspace]).unwrap(),
|
||||
cf_options(CfProfile::Counter, &cache, write_buffer_size),
|
||||
));
|
||||
}
|
||||
|
||||
// Blobs
|
||||
let mut cf_opts = cf_options(CfProfile::Blob, &cache, write_buffer_size);
|
||||
cf_opts.set_enable_blob_files(true);
|
||||
cf_opts.set_min_blob_size(config.blob_size);
|
||||
cf_opts.set_enable_blob_gc(true);
|
||||
cf_opts.set_blob_gc_age_cutoff(1.0);
|
||||
cf_opts.set_blob_gc_force_threshold(0.5);
|
||||
cfs.push(ColumnFamilyDescriptor::new(CF_BLOBS, cf_opts));
|
||||
|
||||
// Other cfs
|
||||
for (subspace, profile) in [
|
||||
(SUBSPACE_INDEXES, CfProfile::Scan),
|
||||
(SUBSPACE_ACL, CfProfile::Scan),
|
||||
(SUBSPACE_TASK_QUEUE, CfProfile::Churn),
|
||||
(SUBSPACE_DELETED_ITEMS, CfProfile::Churn),
|
||||
(SUBSPACE_BLOB_LINK, CfProfile::Churn),
|
||||
(SUBSPACE_IN_MEMORY_VALUE, CfProfile::Churn),
|
||||
(SUBSPACE_PROPERTY, CfProfile::PointLookup),
|
||||
(SUBSPACE_REGISTRY, CfProfile::PointLookup),
|
||||
(SUBSPACE_QUEUE_MESSAGE, CfProfile::Churn),
|
||||
(SUBSPACE_QUEUE_EVENT, CfProfile::Queue),
|
||||
(SUBSPACE_REPORT_OUT, CfProfile::Churn),
|
||||
(SUBSPACE_REPORT_IN, CfProfile::Churn),
|
||||
(SUBSPACE_LOGS, CfProfile::Scan),
|
||||
(SUBSPACE_TELEMETRY_SPAN, CfProfile::PointLookup),
|
||||
(SUBSPACE_TELEMETRY_METRIC, CfProfile::Scan),
|
||||
(SUBSPACE_SEARCH_INDEX, CfProfile::Scan),
|
||||
(SUBSPACE_SPAM_SAMPLES, CfProfile::Churn),
|
||||
(SUBSPACE_REGISTRY_IDX, CfProfile::Scan),
|
||||
(SUBSPACE_REGISTRY_PK, CfProfile::PointLookup),
|
||||
(SUBSPACE_DIRECTORY, CfProfile::PointLookup),
|
||||
(LEGACY_SUBSPACE_BITMAP_TEXT, CfProfile::Scan),
|
||||
(LEGACY_SUBSPACE_BITMAP_TAG, CfProfile::Scan),
|
||||
] {
|
||||
cfs.push(ColumnFamilyDescriptor::new(
|
||||
std::str::from_utf8(&[subspace]).unwrap(),
|
||||
cf_options(profile, &cache, write_buffer_size),
|
||||
));
|
||||
}
|
||||
|
||||
let mut db_opts = Options::default();
|
||||
db_opts.create_missing_column_families(true);
|
||||
db_opts.create_if_missing(true);
|
||||
db_opts.set_max_background_jobs(std::cmp::max(num_cpus::get() as i32, 3));
|
||||
db_opts.increase_parallelism(std::cmp::max(num_cpus::get() as i32, 3));
|
||||
db_opts
|
||||
.set_db_write_buffer_size((config.buffer_size as usize).max(MIN_DB_WRITE_BUFFER_SIZE));
|
||||
db_opts.set_bytes_per_sync(BYTES_PER_SYNC);
|
||||
db_opts.set_wal_bytes_per_sync(BYTES_PER_SYNC);
|
||||
|
||||
Ok(Store::RocksDb(Arc::new(RocksDbStore {
|
||||
db: OptimisticTransactionDB::open_cf_descriptors(&db_opts, idx_path, cfs)
|
||||
.map_err(|err| format!("Failed to open database: {:?}", err))?
|
||||
.into(),
|
||||
worker_pool: rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(std::cmp::max(
|
||||
config
|
||||
.pool_workers
|
||||
.filter(|v| *v > 0)
|
||||
.map(|v| v as usize)
|
||||
.unwrap_or_else(num_cpus::get),
|
||||
4,
|
||||
))
|
||||
.build()
|
||||
.map_err(|err| format!("Failed to build worker pool: {:?}", err))?,
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn spawn_worker<U, V>(&self, mut f: U) -> trc::Result<V>
|
||||
where
|
||||
U: FnMut() -> trc::Result<V> + Send,
|
||||
V: Sync + Send + 'static,
|
||||
{
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
self.worker_pool.scope(|s| {
|
||||
s.spawn(|_| {
|
||||
tx.send(f()).ok();
|
||||
});
|
||||
});
|
||||
|
||||
match rx.await {
|
||||
Ok(result) => result,
|
||||
Err(err) => Err(trc::EventType::Server(trc::ServerEvent::ThreadError).reason(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn numeric_value_merge(
|
||||
_key: &[u8],
|
||||
value: Option<&[u8]>,
|
||||
operands: &MergeOperands,
|
||||
) -> Option<Vec<u8>> {
|
||||
let mut value = if let Some(value) = value {
|
||||
i64::from_le_bytes(value.try_into().ok()?)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
for op in operands.iter() {
|
||||
value += i64::from_le_bytes(op.try_into().ok()?);
|
||||
}
|
||||
|
||||
let mut bytes = Vec::with_capacity(std::mem::size_of::<i64>());
|
||||
bytes.extend_from_slice(&value.to_le_bytes());
|
||||
Some(bytes)
|
||||
}
|
||||
|
||||
fn cf_options(profile: CfProfile, cache: &Cache, write_buffer_size: usize) -> Options {
|
||||
let mut block_opts = BlockBasedOptions::default();
|
||||
block_opts.set_block_cache(cache);
|
||||
block_opts.set_cache_index_and_filter_blocks(true);
|
||||
block_opts.set_pin_l0_filter_and_index_blocks_in_cache(true);
|
||||
|
||||
let mut opts = Options::default();
|
||||
opts.set_write_buffer_size(write_buffer_size);
|
||||
opts.set_max_write_buffer_number(4);
|
||||
|
||||
match profile {
|
||||
CfProfile::PointLookup => {
|
||||
block_opts.set_bloom_filter(BLOOM_BITS_PER_KEY, false);
|
||||
opts.set_compression_type(DBCompressionType::Lz4);
|
||||
}
|
||||
CfProfile::Scan => {
|
||||
block_opts.set_block_size(SCAN_BLOCK_SIZE);
|
||||
opts.set_compression_type(DBCompressionType::Lz4);
|
||||
}
|
||||
CfProfile::Churn => {
|
||||
block_opts.set_bloom_filter(BLOOM_BITS_PER_KEY, false);
|
||||
opts.set_compression_type(DBCompressionType::Lz4);
|
||||
opts.set_target_file_size_base(CHURN_TARGET_FILE_SIZE);
|
||||
opts.add_compact_on_deletion_collector_factory(
|
||||
CHURN_DELETION_WINDOW,
|
||||
CHURN_DELETION_TRIGGER,
|
||||
CHURN_DELETION_RATIO,
|
||||
);
|
||||
}
|
||||
CfProfile::Queue => {
|
||||
block_opts.set_block_size(SCAN_BLOCK_SIZE);
|
||||
opts.set_compression_type(DBCompressionType::None);
|
||||
opts.set_target_file_size_base(CHURN_TARGET_FILE_SIZE);
|
||||
opts.add_compact_on_deletion_collector_factory(
|
||||
CHURN_DELETION_WINDOW,
|
||||
CHURN_DELETION_TRIGGER,
|
||||
CHURN_DELETION_RATIO,
|
||||
);
|
||||
}
|
||||
CfProfile::Counter => {
|
||||
block_opts.set_bloom_filter(BLOOM_BITS_PER_KEY, false);
|
||||
opts.set_compression_type(DBCompressionType::None);
|
||||
opts.set_merge_operator_associative("merge", numeric_value_merge);
|
||||
}
|
||||
CfProfile::Blob => {
|
||||
block_opts.set_bloom_filter(BLOOM_BITS_PER_KEY, false);
|
||||
opts.set_compression_type(DBCompressionType::None);
|
||||
}
|
||||
}
|
||||
|
||||
opts.set_block_based_table_factory(&block_opts);
|
||||
|
||||
opts
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use rocksdb::{BoundColumnFamily, MultiThreaded, OptimisticTransactionDB};
|
||||
|
||||
use crate::{SUBSPACE_BLOBS, SUBSPACE_INDEXES, SUBSPACE_LOGS};
|
||||
|
||||
pub mod blob;
|
||||
pub mod main;
|
||||
pub mod read;
|
||||
pub mod write;
|
||||
|
||||
static CF_LOGS: &str = unsafe { std::str::from_utf8_unchecked(&[SUBSPACE_LOGS]) };
|
||||
static CF_INDEXES: &str = unsafe { std::str::from_utf8_unchecked(&[SUBSPACE_INDEXES]) };
|
||||
static CF_BLOBS: &str = unsafe { std::str::from_utf8_unchecked(&[SUBSPACE_BLOBS]) };
|
||||
|
||||
pub(crate) trait CfHandle {
|
||||
fn subspace_handle(&self, subspace: u8) -> Arc<BoundColumnFamily<'_>>;
|
||||
}
|
||||
|
||||
impl CfHandle for OptimisticTransactionDB<MultiThreaded> {
|
||||
#[inline(always)]
|
||||
fn subspace_handle(&self, subspace: u8) -> Arc<BoundColumnFamily<'_>> {
|
||||
let subspace = &[subspace];
|
||||
self.cf_handle(unsafe { std::str::from_utf8_unchecked(subspace) })
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RocksDbStore {
|
||||
db: Arc<OptimisticTransactionDB<MultiThreaded>>,
|
||||
worker_pool: rayon::ThreadPool,
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn into_error(err: rocksdb::Error) -> trc::Error {
|
||||
trc::StoreEvent::RocksdbError.reason(err)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{RocksDbStore, into_error};
|
||||
use crate::{
|
||||
Deserialize, IterateParams, Key, ValueKey, backend::rocksdb::CfHandle, write::ValueClass,
|
||||
};
|
||||
use rocksdb::ReadOptions;
|
||||
|
||||
impl RocksDbStore {
|
||||
pub(crate) async fn get_value<U>(&self, key: impl Key) -> trc::Result<Option<U>>
|
||||
where
|
||||
U: Deserialize + 'static,
|
||||
{
|
||||
let db = self.db.clone();
|
||||
self.spawn_worker(move || {
|
||||
let subspace = &[key.subspace()];
|
||||
let key = key.serialize(0);
|
||||
db.get_pinned_cf(
|
||||
&db.cf_handle(unsafe { std::str::from_utf8_unchecked(subspace.as_slice()) })
|
||||
.unwrap(),
|
||||
&key,
|
||||
)
|
||||
.map_err(into_error)
|
||||
.and_then(|value| {
|
||||
if let Some(value) = value {
|
||||
U::deserialize_with_key(&key, &value).map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn key_exists(&self, key: impl Key) -> trc::Result<bool> {
|
||||
let db = self.db.clone();
|
||||
self.spawn_worker(move || {
|
||||
let subspace = &[key.subspace()];
|
||||
let key = key.serialize(0);
|
||||
db.get_pinned_cf(
|
||||
&db.cf_handle(unsafe { std::str::from_utf8_unchecked(subspace.as_slice()) })
|
||||
.unwrap(),
|
||||
&key,
|
||||
)
|
||||
.map_err(into_error)
|
||||
.map(|value| value.is_some())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn iterate<T: Key>(
|
||||
&self,
|
||||
params: IterateParams<T>,
|
||||
mut cb: impl for<'x> FnMut(&'x [u8], &'x [u8]) -> trc::Result<bool> + Sync + Send,
|
||||
) -> trc::Result<()> {
|
||||
let db = self.db.clone();
|
||||
|
||||
self.spawn_worker(move || {
|
||||
let cf = db.subspace_handle(params.begin.subspace());
|
||||
let begin = params.begin.serialize(0);
|
||||
let end = params.end.serialize(0);
|
||||
let mut upper_bound = Vec::with_capacity(end.len() + 1);
|
||||
|
||||
upper_bound.extend_from_slice(&end);
|
||||
upper_bound.push(0u8);
|
||||
|
||||
let mut read_opts = ReadOptions::default();
|
||||
read_opts.set_iterate_lower_bound(begin.as_slice());
|
||||
read_opts.set_iterate_upper_bound(upper_bound);
|
||||
|
||||
let mut it = db.raw_iterator_cf_opt(&cf, read_opts);
|
||||
if params.ascending {
|
||||
it.seek(&begin);
|
||||
} else {
|
||||
it.seek_for_prev(&end);
|
||||
}
|
||||
|
||||
while it.valid() {
|
||||
let Some(key) = it.key() else {
|
||||
break;
|
||||
};
|
||||
let value = if params.values {
|
||||
it.value().unwrap_or_default()
|
||||
} else {
|
||||
&[][..]
|
||||
};
|
||||
|
||||
if !cb(key, value)? || params.first {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if params.ascending {
|
||||
it.next();
|
||||
} else {
|
||||
it.prev();
|
||||
}
|
||||
}
|
||||
|
||||
it.status().map_err(into_error)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_counter(
|
||||
&self,
|
||||
key: impl Into<ValueKey<ValueClass>> + Sync + Send,
|
||||
) -> trc::Result<i64> {
|
||||
let key = key.into();
|
||||
let db = self.db.clone();
|
||||
self.spawn_worker(move || {
|
||||
let cf = self.db.subspace_handle(key.subspace());
|
||||
let key = key.serialize(0);
|
||||
|
||||
db.get_pinned_cf(&cf, &key)
|
||||
.map_err(into_error)
|
||||
.and_then(|bytes| {
|
||||
Ok(if let Some(bytes) = bytes {
|
||||
i64::from_le_bytes(bytes[..].try_into().map_err(|_| {
|
||||
trc::Error::corrupted_key(&key, (&bytes[..]).into(), trc::location!())
|
||||
})?)
|
||||
} else {
|
||||
0
|
||||
})
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{CF_INDEXES, CF_LOGS, CfHandle, RocksDbStore, into_error};
|
||||
use crate::{
|
||||
Deserialize, IndexKey, Key, LogKey, SUBSPACE_COUNTER, SUBSPACE_IN_MEMORY_COUNTER,
|
||||
SUBSPACE_QUOTA,
|
||||
backend::deserialize_i64_le,
|
||||
write::{
|
||||
AssignedIds, Batch, MAX_COMMIT_ATTEMPTS, MAX_COMMIT_TIME, MergeResult, Operation,
|
||||
ValueClass, ValueOp,
|
||||
},
|
||||
};
|
||||
use rand::RngExt;
|
||||
use rocksdb::{
|
||||
BoundColumnFamily, ErrorKind, IteratorMode, OptimisticTransactionDB,
|
||||
OptimisticTransactionOptions, WriteOptions,
|
||||
};
|
||||
use std::{
|
||||
sync::Arc,
|
||||
thread::sleep,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
impl RocksDbStore {
|
||||
pub(crate) async fn write(&self, mut batch: Batch<'_>) -> trc::Result<AssignedIds> {
|
||||
let db = self.db.clone();
|
||||
|
||||
self.spawn_worker(move || {
|
||||
let mut txn = RocksDBTransaction {
|
||||
db: &db,
|
||||
cf_indexes: db.cf_handle(CF_INDEXES).unwrap(),
|
||||
cf_logs: db.cf_handle(CF_LOGS).unwrap(),
|
||||
txn_opts: OptimisticTransactionOptions::default(),
|
||||
batch: &mut batch,
|
||||
};
|
||||
txn.txn_opts.set_snapshot(true);
|
||||
|
||||
// Begin write
|
||||
let mut retry_count = 0;
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
match txn.commit() {
|
||||
Ok(result) => {
|
||||
return Ok(result);
|
||||
}
|
||||
Err(CommitError::Internal(err)) => return Err(err),
|
||||
Err(CommitError::RocksDB(err)) => match err.kind() {
|
||||
ErrorKind::Busy | ErrorKind::MergeInProgress | ErrorKind::TryAgain
|
||||
if retry_count < MAX_COMMIT_ATTEMPTS
|
||||
&& start.elapsed() < MAX_COMMIT_TIME =>
|
||||
{
|
||||
let backoff = rand::rng().random_range(50..=300);
|
||||
sleep(Duration::from_millis(backoff));
|
||||
retry_count += 1;
|
||||
}
|
||||
_ => return Err(into_error(err)),
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_range(&self, from: impl Key, to: impl Key) -> trc::Result<()> {
|
||||
let db = self.db.clone();
|
||||
self.spawn_worker(move || {
|
||||
db.delete_range_cf(
|
||||
&db.cf_handle(std::str::from_utf8(&[from.subspace()]).unwrap())
|
||||
.unwrap(),
|
||||
from.serialize(0),
|
||||
to.serialize(0),
|
||||
)
|
||||
.map_err(into_error)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn purge_store(&self) -> trc::Result<()> {
|
||||
let db = self.db.clone();
|
||||
self.spawn_worker(move || {
|
||||
for subspace in [SUBSPACE_QUOTA, SUBSPACE_COUNTER, SUBSPACE_IN_MEMORY_COUNTER] {
|
||||
let cf = db
|
||||
.cf_handle(std::str::from_utf8(&[subspace]).unwrap())
|
||||
.unwrap();
|
||||
|
||||
let mut delete_keys = Vec::new();
|
||||
|
||||
for row in db.iterator_cf(&cf, IteratorMode::Start) {
|
||||
let (key, value) = row.map_err(into_error)?;
|
||||
|
||||
if i64::deserialize(&value)? == 0 {
|
||||
delete_keys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
let txn_opts = OptimisticTransactionOptions::default();
|
||||
for key in delete_keys {
|
||||
let txn = db.transaction_opt(&WriteOptions::default(), &txn_opts);
|
||||
if txn
|
||||
.get_pinned_for_update_cf(&cf, &key, true)
|
||||
.map_err(into_error)?
|
||||
.map(|value| i64::deserialize(&value).map(|v| v == 0).unwrap_or(false))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
txn.delete_cf(&cf, key).map_err(into_error)?;
|
||||
txn.commit().map_err(into_error)?;
|
||||
} else {
|
||||
txn.rollback().map_err(into_error)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
struct RocksDBTransaction<'x, 'y> {
|
||||
db: &'x OptimisticTransactionDB,
|
||||
cf_indexes: Arc<BoundColumnFamily<'x>>,
|
||||
cf_logs: Arc<BoundColumnFamily<'x>>,
|
||||
txn_opts: OptimisticTransactionOptions,
|
||||
batch: &'x mut Batch<'y>,
|
||||
}
|
||||
|
||||
enum CommitError {
|
||||
Internal(trc::Error),
|
||||
RocksDB(rocksdb::Error),
|
||||
}
|
||||
|
||||
impl RocksDBTransaction<'_, '_> {
|
||||
fn commit(&mut self) -> Result<AssignedIds, CommitError> {
|
||||
let mut account_id = u32::MAX;
|
||||
let mut collection = u8::MAX;
|
||||
let mut document_id = u32::MAX;
|
||||
let mut change_id = 0u64;
|
||||
let mut result = AssignedIds::default();
|
||||
let has_changes = !self.batch.changes.is_empty();
|
||||
|
||||
let txn = self
|
||||
.db
|
||||
.transaction_opt(&WriteOptions::default(), &self.txn_opts);
|
||||
|
||||
if has_changes {
|
||||
let cf = self.db.cf_handle("n").unwrap();
|
||||
for &account_id in self.batch.changes.keys() {
|
||||
let key = ValueClass::ChangeId.serialize(account_id, 0, 0, 0);
|
||||
let change_id = txn
|
||||
.get_pinned_for_update_cf(&cf, &key, true)
|
||||
.map_err(CommitError::from)
|
||||
.and_then(|bytes| {
|
||||
if let Some(bytes) = bytes {
|
||||
deserialize_i64_le(&key, &bytes)
|
||||
.map(|v| v + 1)
|
||||
.map_err(CommitError::from)
|
||||
} else {
|
||||
Ok(1)
|
||||
}
|
||||
})?;
|
||||
txn.put_cf(&cf, &key, &change_id.to_le_bytes()[..])?;
|
||||
result.push_change_id(account_id, change_id as u64);
|
||||
}
|
||||
}
|
||||
|
||||
for op in self.batch.ops.iter_mut() {
|
||||
match op {
|
||||
Operation::AccountId {
|
||||
account_id: account_id_,
|
||||
} => {
|
||||
account_id = *account_id_;
|
||||
if has_changes {
|
||||
change_id = result.set_current_change_id(account_id)?;
|
||||
}
|
||||
}
|
||||
Operation::Collection {
|
||||
collection: collection_,
|
||||
} => {
|
||||
collection = u8::from(*collection_);
|
||||
}
|
||||
Operation::DocumentId {
|
||||
document_id: document_id_,
|
||||
} => {
|
||||
document_id = *document_id_;
|
||||
}
|
||||
Operation::Value { class, op } => {
|
||||
let key = class.serialize(account_id, collection, document_id, 0);
|
||||
let cf = self.db.subspace_handle(class.subspace(collection));
|
||||
|
||||
match op {
|
||||
ValueOp::Set(value) => {
|
||||
txn.put_cf(&cf, &key, value)?;
|
||||
}
|
||||
ValueOp::SetFnc(set_op) => {
|
||||
let value = (set_op.fnc)(&set_op.params, &result)?;
|
||||
|
||||
txn.put_cf(&cf, &key, value)?;
|
||||
}
|
||||
ValueOp::MergeFnc(merge_op) => {
|
||||
let merge_result = (merge_op.fnc)(
|
||||
&merge_op.params,
|
||||
&result,
|
||||
txn.get_pinned_for_update_cf(&cf, &key, true)?.as_deref(),
|
||||
)?;
|
||||
|
||||
match merge_result {
|
||||
MergeResult::Update(value) => {
|
||||
txn.put_cf(&cf, &key, value)?;
|
||||
}
|
||||
MergeResult::Delete => {
|
||||
txn.delete_cf(&cf, &key)?;
|
||||
}
|
||||
MergeResult::Skip => (),
|
||||
}
|
||||
}
|
||||
ValueOp::AtomicAdd(by) => {
|
||||
txn.merge_cf(&cf, &key, &by.to_le_bytes()[..])?;
|
||||
}
|
||||
ValueOp::AddAndGet(by) => {
|
||||
let num = txn
|
||||
.get_pinned_for_update_cf(&cf, &key, true)
|
||||
.map_err(CommitError::from)
|
||||
.and_then(|bytes| {
|
||||
if let Some(bytes) = bytes {
|
||||
deserialize_i64_le(&key, &bytes)
|
||||
.map(|v| v + *by)
|
||||
.map_err(CommitError::from)
|
||||
} else {
|
||||
Ok(*by)
|
||||
}
|
||||
})?;
|
||||
txn.put_cf(&cf, &key, &num.to_le_bytes()[..])?;
|
||||
result.push_counter_id(num);
|
||||
}
|
||||
ValueOp::Clear => {
|
||||
txn.delete_cf(&cf, &key)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Operation::Index { field, key, set } => {
|
||||
let key = IndexKey {
|
||||
account_id,
|
||||
collection,
|
||||
document_id,
|
||||
field: *field,
|
||||
key: &*key,
|
||||
}
|
||||
.serialize(0);
|
||||
|
||||
if *set {
|
||||
txn.put_cf(&self.cf_indexes, &key, [])?;
|
||||
} else {
|
||||
txn.delete_cf(&self.cf_indexes, &key)?;
|
||||
}
|
||||
}
|
||||
Operation::Log { collection, set } => {
|
||||
let key = LogKey {
|
||||
account_id,
|
||||
collection: u8::from(*collection),
|
||||
change_id,
|
||||
}
|
||||
.serialize(0);
|
||||
|
||||
txn.put_cf(&self.cf_logs, &key, set)?;
|
||||
}
|
||||
Operation::AssertValue {
|
||||
class,
|
||||
assert_value,
|
||||
} => {
|
||||
let key = class.serialize(account_id, collection, document_id, 0);
|
||||
let cf = self.db.subspace_handle(class.subspace(collection));
|
||||
|
||||
let matches = txn
|
||||
.get_pinned_for_update_cf(&cf, &key, true)?
|
||||
.map(|value| assert_value.matches(&value))
|
||||
.unwrap_or_else(|| assert_value.is_none());
|
||||
|
||||
if !matches {
|
||||
txn.rollback()?;
|
||||
return Err(CommitError::Internal(
|
||||
trc::StoreEvent::AssertValueFailed.into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
txn.commit().map(|_| result).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<rocksdb::Error> for CommitError {
|
||||
fn from(err: rocksdb::Error) -> Self {
|
||||
CommitError::RocksDB(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<trc::Error> for CommitError {
|
||||
fn from(err: trc::Error) -> Self {
|
||||
CommitError::Internal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::BlobStore;
|
||||
use registry::schema::structs;
|
||||
use s3::{Bucket, Region, creds::Credentials};
|
||||
use std::{io::Write, ops::Range, sync::Arc, time::Duration};
|
||||
use utils::codec::base32_custom::Base32Writer;
|
||||
|
||||
pub struct S3Store {
|
||||
bucket: Box<Bucket>,
|
||||
prefix: Option<String>,
|
||||
max_retries: u32,
|
||||
verify_after_write: bool,
|
||||
}
|
||||
|
||||
impl S3Store {
|
||||
pub async fn open(config: structs::S3Store) -> Result<BlobStore, String> {
|
||||
// Obtain region and endpoint from config
|
||||
let region = match config.region {
|
||||
structs::S3StoreRegion::UsEast1 => Region::UsEast1,
|
||||
structs::S3StoreRegion::UsEast2 => Region::UsEast2,
|
||||
structs::S3StoreRegion::UsWest1 => Region::UsWest1,
|
||||
structs::S3StoreRegion::UsWest2 => Region::UsWest2,
|
||||
structs::S3StoreRegion::CaCentral1 => Region::CaCentral1,
|
||||
structs::S3StoreRegion::AfSouth1 => Region::Custom {
|
||||
region: "af-south-1".into(),
|
||||
endpoint: "s3.af-south-1.amazonaws.com".into(),
|
||||
},
|
||||
structs::S3StoreRegion::ApEast1 => Region::ApEast1,
|
||||
structs::S3StoreRegion::ApSouth1 => Region::ApSouth1,
|
||||
structs::S3StoreRegion::ApNortheast1 => Region::ApNortheast1,
|
||||
structs::S3StoreRegion::ApNortheast2 => Region::ApNortheast2,
|
||||
structs::S3StoreRegion::ApNortheast3 => Region::ApNortheast3,
|
||||
structs::S3StoreRegion::ApSoutheast1 => Region::ApSoutheast1,
|
||||
structs::S3StoreRegion::ApSoutheast2 => Region::ApSoutheast2,
|
||||
structs::S3StoreRegion::CnNorth1 => Region::CnNorth1,
|
||||
structs::S3StoreRegion::CnNorthwest1 => Region::CnNorthwest1,
|
||||
structs::S3StoreRegion::EuNorth1 => Region::EuNorth1,
|
||||
structs::S3StoreRegion::EuCentral1 => Region::EuCentral1,
|
||||
structs::S3StoreRegion::EuCentral2 => Region::EuCentral2,
|
||||
structs::S3StoreRegion::EuWest1 => Region::EuWest1,
|
||||
structs::S3StoreRegion::EuWest2 => Region::EuWest2,
|
||||
structs::S3StoreRegion::EuWest3 => Region::EuWest3,
|
||||
structs::S3StoreRegion::IlCentral1 => Region::IlCentral1,
|
||||
structs::S3StoreRegion::MeSouth1 => Region::MeSouth1,
|
||||
structs::S3StoreRegion::SaEast1 => Region::SaEast1,
|
||||
structs::S3StoreRegion::DoNyc3 => Region::DoNyc3,
|
||||
structs::S3StoreRegion::DoAms3 => Region::DoAms3,
|
||||
structs::S3StoreRegion::DoSgp1 => Region::DoSgp1,
|
||||
structs::S3StoreRegion::DoFra1 => Region::DoFra1,
|
||||
structs::S3StoreRegion::Yandex => Region::Yandex,
|
||||
structs::S3StoreRegion::WaUsEast1 => Region::WaUsEast1,
|
||||
structs::S3StoreRegion::WaUsEast2 => Region::WaUsEast2,
|
||||
structs::S3StoreRegion::WaUsCentral1 => Region::WaUsCentral1,
|
||||
structs::S3StoreRegion::WaUsWest1 => Region::WaUsWest1,
|
||||
structs::S3StoreRegion::WaCaCentral1 => Region::WaCaCentral1,
|
||||
structs::S3StoreRegion::WaEuCentral1 => Region::WaEuCentral1,
|
||||
structs::S3StoreRegion::WaEuCentral2 => Region::WaEuCentral2,
|
||||
structs::S3StoreRegion::WaEuWest1 => Region::WaEuWest1,
|
||||
structs::S3StoreRegion::WaEuWest2 => Region::WaEuWest2,
|
||||
structs::S3StoreRegion::WaApNortheast1 => Region::WaApNortheast1,
|
||||
structs::S3StoreRegion::WaApNortheast2 => Region::WaApNortheast2,
|
||||
structs::S3StoreRegion::WaApSoutheast1 => Region::WaApSoutheast1,
|
||||
structs::S3StoreRegion::WaApSoutheast2 => Region::WaApSoutheast2,
|
||||
structs::S3StoreRegion::Custom(custom) => Region::Custom {
|
||||
region: custom.custom_region,
|
||||
endpoint: custom.custom_endpoint,
|
||||
},
|
||||
};
|
||||
let credentials = Credentials::new(
|
||||
config.access_key.value().await?.as_deref(),
|
||||
config.secret_key.secret().await?.as_deref(),
|
||||
config.security_token.secret().await?.as_deref(),
|
||||
config.session_token.secret().await?.as_deref(),
|
||||
config.profile.as_deref(),
|
||||
)
|
||||
.map_err(|err| format!("Failed to create credentials: {err:?}"))?;
|
||||
|
||||
Ok(BlobStore::S3(Arc::new(S3Store {
|
||||
bucket: Bucket::new(&config.bucket, region, credentials)
|
||||
.map_err(|err| format!("Failed to create bucket: {err:?}"))?
|
||||
.with_path_style()
|
||||
.set_dangerous_config(config.allow_invalid_certs, config.allow_invalid_certs)
|
||||
.map_err(|err| format!("Failed to create bucket: {err:?}"))?
|
||||
.with_request_timeout(config.timeout.into_inner())
|
||||
.map_err(|err| format!("Failed to create bucket: {err:?}"))?,
|
||||
max_retries: config.max_retries as u32,
|
||||
prefix: config.key_prefix,
|
||||
verify_after_write: config.verify_after_write,
|
||||
})))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_blob(
|
||||
&self,
|
||||
key: &[u8],
|
||||
range: Range<usize>,
|
||||
) -> trc::Result<Option<Vec<u8>>> {
|
||||
let path = self.build_key(key);
|
||||
let mut retries_left = self.max_retries;
|
||||
|
||||
loop {
|
||||
let response = if range.start != 0 || range.end != usize::MAX {
|
||||
self.bucket
|
||||
.get_object_range(
|
||||
&path,
|
||||
range.start as u64,
|
||||
Some(range.end.saturating_sub(1) as u64),
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
self.bucket.get_object(&path).await
|
||||
}
|
||||
.map_err(into_error)?;
|
||||
|
||||
match response.status_code() {
|
||||
200..=299 => return Ok(Some(response.to_vec())),
|
||||
404 => return Ok(None),
|
||||
500..=599 if retries_left > 0 => {
|
||||
// wait backoff
|
||||
tokio::time::sleep(Duration::from_secs(
|
||||
1 << (self.max_retries - retries_left).min(6),
|
||||
))
|
||||
.await;
|
||||
|
||||
retries_left -= 1;
|
||||
}
|
||||
code => {
|
||||
return Err(trc::StoreEvent::S3Error
|
||||
.reason(String::from_utf8_lossy(response.as_slice()))
|
||||
.ctx(trc::Key::Code, code));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn put_blob(&self, key: &[u8], data: &[u8]) -> trc::Result<()> {
|
||||
let path = self.build_key(key);
|
||||
let mut retries_left = self.max_retries;
|
||||
|
||||
loop {
|
||||
let response = self
|
||||
.bucket
|
||||
.put_object(&path, data)
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
|
||||
match response.status_code() {
|
||||
200..=299 => {
|
||||
if !self.verify_after_write {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Some S3-compatible backends acknowledge a PUT before the
|
||||
// write is durable. HEAD the object to confirm it is visible
|
||||
// to the read path before reporting success.
|
||||
let (_, head_status) =
|
||||
self.bucket.head_object(&path).await.map_err(into_error)?;
|
||||
|
||||
match head_status {
|
||||
200..=299 => return Ok(()),
|
||||
404 | 500..=599 if retries_left > 0 => {
|
||||
tokio::time::sleep(Duration::from_secs(
|
||||
1 << (self.max_retries - retries_left).min(6),
|
||||
))
|
||||
.await;
|
||||
|
||||
retries_left -= 1;
|
||||
}
|
||||
404 => {
|
||||
return Err(trc::StoreEvent::S3Error
|
||||
.reason(concat!(
|
||||
"PUT acknowledged with 2xx but object not visible",
|
||||
"to read path; backend may be silently losing writes"
|
||||
))
|
||||
.ctx(trc::Key::Code, head_status));
|
||||
}
|
||||
code => {
|
||||
return Err(trc::StoreEvent::S3Error
|
||||
.reason("HEAD verification failed after PUT")
|
||||
.ctx(trc::Key::Code, code));
|
||||
}
|
||||
}
|
||||
}
|
||||
500..=599 if retries_left > 0 => {
|
||||
// wait backoff
|
||||
tokio::time::sleep(Duration::from_secs(
|
||||
1 << (self.max_retries - retries_left).min(6),
|
||||
))
|
||||
.await;
|
||||
|
||||
retries_left -= 1;
|
||||
}
|
||||
code => {
|
||||
return Err(trc::StoreEvent::S3Error
|
||||
.reason(String::from_utf8_lossy(response.as_slice()))
|
||||
.ctx(trc::Key::Code, code));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_blob(&self, key: &[u8]) -> trc::Result<bool> {
|
||||
let mut retries_left = self.max_retries;
|
||||
|
||||
loop {
|
||||
let response = self
|
||||
.bucket
|
||||
.delete_object(self.build_key(key))
|
||||
.await
|
||||
.map_err(into_error)?;
|
||||
|
||||
match response.status_code() {
|
||||
200..=299 => return Ok(true),
|
||||
404 => return Ok(false),
|
||||
500..=599 if retries_left > 0 => {
|
||||
// wait backoff
|
||||
tokio::time::sleep(Duration::from_secs(
|
||||
1 << (self.max_retries - retries_left).min(6),
|
||||
))
|
||||
.await;
|
||||
|
||||
retries_left -= 1;
|
||||
}
|
||||
code => {
|
||||
return Err(trc::StoreEvent::S3Error
|
||||
.reason(String::from_utf8_lossy(response.as_slice()))
|
||||
.ctx(trc::Key::Code, code));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_key(&self, key: &[u8]) -> String {
|
||||
if let Some(prefix) = &self.prefix {
|
||||
let mut writer =
|
||||
Base32Writer::with_raw_capacity(prefix.len() + (key.len().div_ceil(4) * 5));
|
||||
writer.push_string(prefix);
|
||||
writer.write_all(key).unwrap();
|
||||
writer.finalize()
|
||||
} else {
|
||||
Base32Writer::from_bytes(key).finalize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn into_error(err: impl std::error::Error) -> trc::Error {
|
||||
let mut reason = err.to_string();
|
||||
let mut source = err.source();
|
||||
while let Some(err) = source {
|
||||
reason.push_str(": ");
|
||||
reason.push_str(&err.to_string());
|
||||
source = err.source();
|
||||
}
|
||||
trc::StoreEvent::S3Error.reason(reason)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use rusqlite::OptionalExtension;
|
||||
|
||||
use super::{SqliteStore, into_error};
|
||||
|
||||
impl SqliteStore {
|
||||
pub(crate) async fn get_blob(
|
||||
&self,
|
||||
key: &[u8],
|
||||
range: Range<usize>,
|
||||
) -> trc::Result<Option<Vec<u8>>> {
|
||||
let manager = self.conn_pool.clone();
|
||||
self.spawn_worker(move || {
|
||||
let conn = manager.get().map_err(into_error)?;
|
||||
let mut result = conn
|
||||
.prepare_cached("SELECT v FROM t WHERE k = ?")
|
||||
.map_err(into_error)?;
|
||||
result
|
||||
.query_row([&key], |row| {
|
||||
Ok({
|
||||
let bytes = row.get_ref(0)?.as_bytes()?;
|
||||
if range.start == 0 && range.end == usize::MAX {
|
||||
bytes.to_vec()
|
||||
} else {
|
||||
bytes
|
||||
.get(range.start..std::cmp::min(bytes.len(), range.end))
|
||||
.unwrap_or_default()
|
||||
.to_vec()
|
||||
}
|
||||
})
|
||||
})
|
||||
.optional()
|
||||
.map_err(into_error)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn put_blob(&self, key: &[u8], data: &[u8]) -> trc::Result<()> {
|
||||
let manager = self.conn_pool.clone();
|
||||
self.spawn_worker(move || {
|
||||
let conn = manager.get().map_err(into_error)?;
|
||||
conn.prepare_cached("INSERT OR REPLACE INTO t (k, v) VALUES (?, ?)")
|
||||
.map_err(into_error)?
|
||||
.execute([key, data])
|
||||
.map_err(into_error)
|
||||
.map(|_| ())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_blob(&self, key: &[u8]) -> trc::Result<bool> {
|
||||
let manager = self.conn_pool.clone();
|
||||
self.spawn_worker(move || {
|
||||
let conn = manager.get().map_err(into_error)?;
|
||||
conn.prepare_cached("DELETE FROM t WHERE k = ?")
|
||||
.map_err(into_error)?
|
||||
.execute([key])
|
||||
.map_err(into_error)
|
||||
.map(|_| true)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use rusqlite::{Row, Rows, ToSql, types::FromSql};
|
||||
|
||||
use crate::{IntoRows, QueryResult, QueryType, Value};
|
||||
|
||||
use super::{SqliteStore, into_error};
|
||||
|
||||
impl SqliteStore {
|
||||
pub(crate) async fn sql_query<T: QueryResult>(
|
||||
&self,
|
||||
query: &str,
|
||||
params_: &[Value<'_>],
|
||||
) -> trc::Result<T> {
|
||||
let manager = self.conn_pool.clone();
|
||||
self.spawn_worker(move || {
|
||||
let conn = manager.get().map_err(into_error)?;
|
||||
let mut s = conn.prepare_cached(query).map_err(into_error)?;
|
||||
let params = params_
|
||||
.iter()
|
||||
.map(|v| v as &dyn rusqlite::types::ToSql)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
match T::query_type() {
|
||||
QueryType::Execute => s
|
||||
.execute(params.as_slice())
|
||||
.map_or_else(|e| Err(into_error(e)), |r| Ok(T::from_exec(r))),
|
||||
QueryType::Exists => s
|
||||
.exists(params.as_slice())
|
||||
.map(T::from_exists)
|
||||
.map_err(into_error),
|
||||
QueryType::QueryOne => s
|
||||
.query(params.as_slice())
|
||||
.and_then(|mut rows| Ok(T::from_query_one(rows.next()?)))
|
||||
.map_err(into_error),
|
||||
QueryType::QueryAll => Ok(T::from_query_all(
|
||||
s.query(params.as_slice()).map_err(into_error)?,
|
||||
)),
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql for Value<'_> {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
||||
match self {
|
||||
Value::Integer(value) => value.to_sql(),
|
||||
Value::Bool(value) => value.to_sql(),
|
||||
Value::Float(value) => value.to_sql(),
|
||||
Value::Text(value) => value.to_sql(),
|
||||
Value::Blob(value) => value.to_sql(),
|
||||
Value::Null => Ok(rusqlite::types::ToSqlOutput::Owned(
|
||||
rusqlite::types::Value::Null,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql for Value<'static> {
|
||||
fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
|
||||
Ok(match value {
|
||||
rusqlite::types::ValueRef::Null => Value::Null,
|
||||
rusqlite::types::ValueRef::Integer(v) => Value::Integer(v),
|
||||
rusqlite::types::ValueRef::Real(v) => Value::Float(v),
|
||||
rusqlite::types::ValueRef::Text(v) => {
|
||||
Value::Text(String::from_utf8_lossy(v).into_owned().into())
|
||||
}
|
||||
rusqlite::types::ValueRef::Blob(v) => Value::Blob(v.to_vec().into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoRows for Rows<'_> {
|
||||
fn into_rows(mut self) -> crate::Rows {
|
||||
let column_count = self.as_ref().map(|s| s.column_count()).unwrap_or_default();
|
||||
let mut rows = crate::Rows { rows: Vec::new() };
|
||||
|
||||
while let Ok(Some(row)) = self.next() {
|
||||
rows.rows.push(crate::Row {
|
||||
values: (0..column_count)
|
||||
.map(|idx| row.get::<_, Value>(idx).unwrap_or(Value::Null))
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
|
||||
fn into_named_rows(mut self) -> crate::NamedRows {
|
||||
let (column_count, names) = self
|
||||
.as_ref()
|
||||
.map(|s| {
|
||||
(
|
||||
s.column_count(),
|
||||
s.column_names()
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
.unwrap_or((0, Vec::new()));
|
||||
|
||||
let mut rows = crate::NamedRows {
|
||||
names,
|
||||
rows: Vec::new(),
|
||||
};
|
||||
|
||||
while let Ok(Some(row)) = self.next() {
|
||||
rows.rows.push(crate::Row {
|
||||
values: (0..column_count)
|
||||
.map(|idx| row.get::<_, Value>(idx).unwrap_or(Value::Null))
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
|
||||
fn into_row(self) -> Option<crate::Row> {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoRows for Option<&Row<'_>> {
|
||||
fn into_row(self) -> Option<crate::Row> {
|
||||
self.map(|row| crate::Row {
|
||||
values: (0..row.as_ref().column_count())
|
||||
.map(|idx| row.get::<_, Value>(idx).unwrap_or(Value::Null))
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn into_rows(self) -> crate::Rows {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn into_named_rows(self) -> crate::NamedRows {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{SqliteStore, into_error, pool::SqliteConnectionManager};
|
||||
use crate::*;
|
||||
use ::registry::schema::structs;
|
||||
use r2d2::Pool;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
impl SqliteStore {
|
||||
pub fn open(config: structs::SqliteStore) -> Result<Store, String> {
|
||||
Ok(Store::SQLite(Arc::new(SqliteStore {
|
||||
conn_pool: Pool::builder()
|
||||
.max_size(config.pool_max_connections as u32)
|
||||
.build(SqliteConnectionManager::file(&config.path).with_init(|c| {
|
||||
c.execute_batch(concat!(
|
||||
"PRAGMA journal_mode = WAL; ",
|
||||
"PRAGMA synchronous = NORMAL; ",
|
||||
"PRAGMA temp_store = memory;",
|
||||
"PRAGMA busy_timeout = 30000;"
|
||||
))
|
||||
}))
|
||||
.map_err(|err| format!("Failed to build connection pool: {err}"))?,
|
||||
worker_pool: rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(std::cmp::max(
|
||||
config
|
||||
.pool_workers
|
||||
.filter(|v| *v > 0)
|
||||
.map(|v| v as usize)
|
||||
.unwrap_or_else(num_cpus::get),
|
||||
4,
|
||||
))
|
||||
.build()
|
||||
.map_err(|err| format!("Failed to build worker pool: {err}"))?,
|
||||
})))
|
||||
}
|
||||
|
||||
#[cfg(feature = "test_mode")]
|
||||
pub fn open_memory() -> trc::Result<Self> {
|
||||
use super::into_error;
|
||||
|
||||
let db = Self {
|
||||
conn_pool: Pool::builder()
|
||||
.max_size(1)
|
||||
.build(SqliteConnectionManager::memory())
|
||||
.map_err(into_error)?,
|
||||
worker_pool: rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(num_cpus::get())
|
||||
.build()
|
||||
.map_err(|err| {
|
||||
into_error(err).ctx(trc::Key::Reason, "Failed to build worker pool")
|
||||
})?,
|
||||
};
|
||||
db.create_tables()?;
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
pub(crate) fn create_tables(&self) -> trc::Result<()> {
|
||||
let conn = self.conn_pool.get().map_err(into_error)?;
|
||||
|
||||
for table in [
|
||||
SUBSPACE_ACL,
|
||||
SUBSPACE_TASK_QUEUE,
|
||||
SUBSPACE_DELETED_ITEMS,
|
||||
SUBSPACE_SPAM_SAMPLES,
|
||||
SUBSPACE_BLOB_LINK,
|
||||
SUBSPACE_IN_MEMORY_VALUE,
|
||||
SUBSPACE_PROPERTY,
|
||||
SUBSPACE_REGISTRY,
|
||||
SUBSPACE_REGISTRY_PK,
|
||||
SUBSPACE_QUEUE_MESSAGE,
|
||||
SUBSPACE_QUEUE_EVENT,
|
||||
SUBSPACE_REPORT_OUT,
|
||||
SUBSPACE_REPORT_IN,
|
||||
SUBSPACE_LOGS,
|
||||
SUBSPACE_BLOBS,
|
||||
SUBSPACE_TELEMETRY_SPAN,
|
||||
SUBSPACE_TELEMETRY_METRIC,
|
||||
SUBSPACE_SEARCH_INDEX,
|
||||
SUBSPACE_DIRECTORY,
|
||||
] {
|
||||
let table = char::from(table);
|
||||
conn.execute(
|
||||
&format!(
|
||||
"CREATE TABLE IF NOT EXISTS {table} (
|
||||
k BLOB PRIMARY KEY,
|
||||
v BLOB NOT NULL
|
||||
)"
|
||||
),
|
||||
[],
|
||||
)
|
||||
.map_err(into_error)?;
|
||||
}
|
||||
|
||||
for table in [SUBSPACE_INDEXES, SUBSPACE_REGISTRY_IDX] {
|
||||
let table = char::from(table);
|
||||
conn.execute(
|
||||
&format!(
|
||||
"CREATE TABLE IF NOT EXISTS {table} (
|
||||
k BLOB PRIMARY KEY
|
||||
)"
|
||||
),
|
||||
[],
|
||||
)
|
||||
.map_err(into_error)?;
|
||||
}
|
||||
|
||||
for table in [SUBSPACE_COUNTER, SUBSPACE_QUOTA, SUBSPACE_IN_MEMORY_COUNTER] {
|
||||
conn.execute(
|
||||
&format!(
|
||||
"CREATE TABLE IF NOT EXISTS {} (
|
||||
k BLOB PRIMARY KEY,
|
||||
v INTEGER NOT NULL DEFAULT 0
|
||||
)",
|
||||
char::from(table)
|
||||
),
|
||||
[],
|
||||
)
|
||||
.map_err(into_error)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn spawn_worker<U, V>(&self, mut f: U) -> trc::Result<V>
|
||||
where
|
||||
U: FnMut() -> trc::Result<V> + Send,
|
||||
V: Sync + Send + 'static,
|
||||
{
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
self.worker_pool.scope(|s| {
|
||||
s.spawn(|_| {
|
||||
tx.send(f()).ok();
|
||||
});
|
||||
});
|
||||
|
||||
match rx.await {
|
||||
Ok(result) => result,
|
||||
Err(err) => Err(trc::EventType::Server(trc::ServerEvent::ThreadError).reason(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use self::pool::SqliteConnectionManager;
|
||||
use r2d2::Pool;
|
||||
use std::fmt::Display;
|
||||
|
||||
pub mod blob;
|
||||
pub mod lookup;
|
||||
pub mod main;
|
||||
pub mod pool;
|
||||
pub mod read;
|
||||
pub mod write;
|
||||
|
||||
pub struct SqliteStore {
|
||||
pub(crate) conn_pool: Pool<SqliteConnectionManager>,
|
||||
pub(crate) worker_pool: rayon::ThreadPool,
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn into_error(err: impl Display) -> trc::Error {
|
||||
trc::StoreEvent::SqliteError.reason(err)
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use rusqlite::{Connection, Error, OpenFlags};
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Source {
|
||||
File(PathBuf),
|
||||
Memory,
|
||||
}
|
||||
|
||||
type InitFn = dyn Fn(&mut Connection) -> Result<(), rusqlite::Error> + Send + Sync + 'static;
|
||||
|
||||
/// An `r2d2::ManageConnection` for `rusqlite::Connection`s.
|
||||
pub struct SqliteConnectionManager {
|
||||
source: Source,
|
||||
flags: OpenFlags,
|
||||
init: Option<Box<InitFn>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for SqliteConnectionManager {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let mut builder = f.debug_struct("SqliteConnectionManager");
|
||||
let _ = builder.field("source", &self.source);
|
||||
let _ = builder.field("flags", &self.source);
|
||||
let _ = builder.field("init", &self.init.as_ref().map(|_| "InitFn"));
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SqliteConnectionManager {
|
||||
/// Creates a new `SqliteConnectionManager` from file.
|
||||
///
|
||||
/// See `rusqlite::Connection::open`
|
||||
pub fn file<P: AsRef<Path>>(path: P) -> Self {
|
||||
Self {
|
||||
source: Source::File(path.as_ref().to_path_buf()),
|
||||
flags: OpenFlags::default(),
|
||||
init: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new `SqliteConnectionManager` from memory.
|
||||
pub fn memory() -> Self {
|
||||
Self {
|
||||
source: Source::Memory,
|
||||
flags: OpenFlags::default(),
|
||||
init: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts `SqliteConnectionManager` into one that sets OpenFlags upon
|
||||
/// connection creation.
|
||||
///
|
||||
/// See `rustqlite::OpenFlags` for a list of available flags.
|
||||
pub fn with_flags(self, flags: OpenFlags) -> Self {
|
||||
Self { flags, ..self }
|
||||
}
|
||||
|
||||
/// Converts `SqliteConnectionManager` into one that calls an initialization
|
||||
/// function upon connection creation. Could be used to set PRAGMAs, for
|
||||
/// example.
|
||||
///
|
||||
/// ### Example
|
||||
///
|
||||
/// Make a `SqliteConnectionManager` that sets the `foreign_keys` pragma to
|
||||
/// true for every connection.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use r2d2_sqlite::{SqliteConnectionManager};
|
||||
/// let manager = SqliteConnectionManager::file("app.db")
|
||||
/// .with_init(|c| c.execute_batch("PRAGMA foreign_keys=1;"));
|
||||
/// ```
|
||||
pub fn with_init<F>(self, init: F) -> Self
|
||||
where
|
||||
F: Fn(&mut Connection) -> Result<(), rusqlite::Error> + Send + Sync + 'static,
|
||||
{
|
||||
let init: Option<Box<InitFn>> = Some(Box::new(init));
|
||||
Self { init, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
fn sleeper(_: i32) -> bool {
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
true
|
||||
}
|
||||
|
||||
impl r2d2::ManageConnection for SqliteConnectionManager {
|
||||
type Connection = Connection;
|
||||
type Error = rusqlite::Error;
|
||||
|
||||
fn connect(&self) -> Result<Connection, Error> {
|
||||
match self.source {
|
||||
Source::File(ref path) => Connection::open_with_flags(path, self.flags),
|
||||
Source::Memory => Connection::open_in_memory_with_flags(self.flags),
|
||||
}
|
||||
.and_then(|mut c| {
|
||||
c.busy_handler(Some(sleeper))?;
|
||||
match self.init {
|
||||
None => Ok(c),
|
||||
Some(ref init) => init(&mut c).map(|_| c),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn is_valid(&self, conn: &mut Connection) -> Result<(), Error> {
|
||||
conn.execute_batch("")
|
||||
}
|
||||
|
||||
fn has_broken(&self, _: &mut Connection) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{SqliteStore, into_error};
|
||||
use crate::{Deserialize, IterateParams, Key, ValueKey, write::ValueClass};
|
||||
use rusqlite::OptionalExtension;
|
||||
|
||||
impl SqliteStore {
|
||||
pub(crate) async fn get_value<U>(&self, key: impl Key) -> trc::Result<Option<U>>
|
||||
where
|
||||
U: Deserialize + 'static,
|
||||
{
|
||||
let manager = self.conn_pool.clone();
|
||||
self.spawn_worker(move || {
|
||||
let conn = manager.get().map_err(into_error)?;
|
||||
let mut result = conn
|
||||
.prepare_cached(&format!(
|
||||
"SELECT v FROM {} WHERE k = ?",
|
||||
char::from(key.subspace())
|
||||
))
|
||||
.map_err(into_error)?;
|
||||
let key = key.serialize(0);
|
||||
result
|
||||
.query_row([&key], |row| {
|
||||
U::deserialize_with_key(&key, row.get_ref(0)?.as_bytes()?)
|
||||
.map_err(|err| rusqlite::Error::ToSqlConversionFailure(err.into()))
|
||||
})
|
||||
.optional()
|
||||
.map_err(into_error)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn key_exists(&self, key: impl Key) -> trc::Result<bool> {
|
||||
let manager = self.conn_pool.clone();
|
||||
self.spawn_worker(move || {
|
||||
let conn = manager.get().map_err(into_error)?;
|
||||
let mut result = conn
|
||||
.prepare_cached(&format!(
|
||||
"SELECT 1 FROM {} WHERE k = ?",
|
||||
char::from(key.subspace())
|
||||
))
|
||||
.map_err(into_error)?;
|
||||
let key = key.serialize(0);
|
||||
result
|
||||
.query_row([&key], |_| Ok(()))
|
||||
.optional()
|
||||
.map(|opt| opt.is_some())
|
||||
.map_err(into_error)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn iterate<T: Key>(
|
||||
&self,
|
||||
params: IterateParams<T>,
|
||||
mut cb: impl for<'x> FnMut(&'x [u8], &'x [u8]) -> trc::Result<bool> + Sync + Send,
|
||||
) -> trc::Result<()> {
|
||||
let manager = self.conn_pool.clone();
|
||||
self.spawn_worker(move || {
|
||||
let conn = manager.get().map_err(into_error)?;
|
||||
let table = char::from(params.begin.subspace());
|
||||
let begin = params.begin.serialize(0);
|
||||
let end = params.end.serialize(0);
|
||||
let keys = if params.values { "k, v" } else { "k" };
|
||||
|
||||
let mut query = conn
|
||||
.prepare_cached(&match (params.first, params.ascending) {
|
||||
(true, true) => {
|
||||
format!(
|
||||
"SELECT {keys} FROM {table} WHERE k >= ? AND k <= ? ORDER BY k ASC LIMIT 1"
|
||||
)
|
||||
}
|
||||
(true, false) => {
|
||||
format!(
|
||||
"SELECT {keys} FROM {table} WHERE k >= ? AND k <= ? ORDER BY k DESC LIMIT 1"
|
||||
)
|
||||
}
|
||||
(false, true) => {
|
||||
format!("SELECT {keys} FROM {table} WHERE k >= ? AND k <= ? ORDER BY k ASC")
|
||||
}
|
||||
(false, false) => {
|
||||
format!(
|
||||
"SELECT {keys} FROM {table} WHERE k >= ? AND k <= ? ORDER BY k DESC"
|
||||
)
|
||||
}
|
||||
})
|
||||
.map_err(into_error)?;
|
||||
let mut rows = query.query([&begin, &end]).map_err(into_error)?;
|
||||
|
||||
if params.values {
|
||||
while let Some(row) = rows.next().map_err(into_error)? {
|
||||
let key = row
|
||||
.get_ref(0)
|
||||
.map_err(into_error)?
|
||||
.as_bytes()
|
||||
.map_err(into_error)?;
|
||||
let value = row
|
||||
.get_ref(1)
|
||||
.map_err(into_error)?
|
||||
.as_bytes()
|
||||
.map_err(into_error)?;
|
||||
|
||||
if !cb(key, value)? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while let Some(row) = rows.next().map_err(into_error)? {
|
||||
if !cb(
|
||||
row.get_ref(0)
|
||||
.map_err(into_error)?
|
||||
.as_bytes()
|
||||
.map_err(into_error)?,
|
||||
b"",
|
||||
)? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_counter(
|
||||
&self,
|
||||
key: impl Into<ValueKey<ValueClass>> + Sync + Send,
|
||||
) -> trc::Result<i64> {
|
||||
let key = key.into();
|
||||
let table = char::from(key.subspace());
|
||||
let key = key.serialize(0);
|
||||
let manager = self.conn_pool.clone();
|
||||
self.spawn_worker(move || {
|
||||
let conn = manager.get().map_err(into_error)?;
|
||||
match conn
|
||||
.prepare_cached(&format!("SELECT v FROM {table} WHERE k = ?"))
|
||||
.map_err(into_error)?
|
||||
.query_row([&key], |row| row.get::<_, i64>(0))
|
||||
{
|
||||
Ok(value) => Ok(value),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(0),
|
||||
Err(e) => Err(into_error(e)),
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{SqliteStore, into_error};
|
||||
use crate::{
|
||||
IndexKey, Key, LogKey, SUBSPACE_COUNTER, SUBSPACE_IN_MEMORY_COUNTER, SUBSPACE_QUOTA,
|
||||
SUBSPACE_REGISTRY_IDX,
|
||||
write::{AssignedIds, Batch, MergeResult, Operation, ValueClass, ValueOp},
|
||||
};
|
||||
use rusqlite::{OptionalExtension, TransactionBehavior, params};
|
||||
use trc::AddContext;
|
||||
|
||||
impl SqliteStore {
|
||||
pub(crate) async fn write(&self, batch: Batch<'_>) -> trc::Result<AssignedIds> {
|
||||
let manager = self.conn_pool.clone();
|
||||
self.spawn_worker(move || {
|
||||
let mut conn = manager.get().map_err(into_error)?;
|
||||
|
||||
let mut account_id = u32::MAX;
|
||||
let mut collection = u8::MAX;
|
||||
let mut document_id = u32::MAX;
|
||||
let mut change_id = 0u64;
|
||||
let trx = conn
|
||||
.transaction_with_behavior(TransactionBehavior::Immediate)
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?;
|
||||
let mut result = AssignedIds::default();
|
||||
let has_changes = !batch.changes.is_empty();
|
||||
|
||||
if has_changes {
|
||||
for &account_id in batch.changes.keys() {
|
||||
let key = ValueClass::ChangeId.serialize(account_id, 0, 0, 0);
|
||||
let change_id = trx
|
||||
.prepare_cached(concat!(
|
||||
"INSERT INTO n (k, v) VALUES (?, ?) ",
|
||||
"ON CONFLICT(k) DO UPDATE SET v = v + ",
|
||||
"excluded.v RETURNING v"
|
||||
))
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?
|
||||
.query_row(params![&key, &1i64], |row| row.get::<_, i64>(0))
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?;
|
||||
result.push_change_id(account_id, change_id as u64);
|
||||
}
|
||||
}
|
||||
|
||||
for op in batch.ops.iter_mut() {
|
||||
match op {
|
||||
Operation::AccountId {
|
||||
account_id: account_id_,
|
||||
} => {
|
||||
account_id = *account_id_;
|
||||
if has_changes {
|
||||
change_id = result.set_current_change_id(account_id)?;
|
||||
}
|
||||
}
|
||||
Operation::Collection {
|
||||
collection: collection_,
|
||||
} => {
|
||||
collection = u8::from(*collection_);
|
||||
}
|
||||
Operation::DocumentId {
|
||||
document_id: document_id_,
|
||||
} => {
|
||||
document_id = *document_id_;
|
||||
}
|
||||
Operation::Value { class, op } => {
|
||||
let key = class.serialize(account_id, collection, document_id, 0);
|
||||
let subspace = class.subspace(collection);
|
||||
let table = char::from(subspace);
|
||||
|
||||
match op {
|
||||
ValueOp::Set(value) => {
|
||||
if subspace != SUBSPACE_REGISTRY_IDX {
|
||||
trx.prepare_cached(&format!(
|
||||
"INSERT OR REPLACE INTO {} (k, v) VALUES (?, ?)",
|
||||
table
|
||||
))
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?
|
||||
.execute([&key, value])
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?;
|
||||
} else {
|
||||
trx.prepare_cached("INSERT OR IGNORE INTO b (k) VALUES (?)")
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?
|
||||
.execute([&key])
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
}
|
||||
ValueOp::SetFnc(set_op) => {
|
||||
let value = (set_op.fnc)(&set_op.params, &result)?;
|
||||
trx.prepare_cached(&format!(
|
||||
"INSERT OR REPLACE INTO {} (k, v) VALUES (?, ?)",
|
||||
table
|
||||
))
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?
|
||||
.execute([&key, &value])
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
ValueOp::MergeFnc(merge_op) => {
|
||||
let merge_result = trx
|
||||
.prepare_cached(&format!("SELECT v FROM {} WHERE k = ?", table))
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?
|
||||
.query_row([&key], |row| {
|
||||
Ok((merge_op.fnc)(
|
||||
&merge_op.params,
|
||||
&result,
|
||||
Some(row.get_ref(0)?.as_bytes()?),
|
||||
))
|
||||
})
|
||||
.optional()
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?
|
||||
.unwrap_or_else(|| {
|
||||
(merge_op.fnc)(&merge_op.params, &result, None)
|
||||
})?;
|
||||
|
||||
match merge_result {
|
||||
MergeResult::Update(value) => {
|
||||
trx.prepare_cached(&format!(
|
||||
"INSERT OR REPLACE INTO {} (k, v) VALUES (?, ?)",
|
||||
table
|
||||
))
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?
|
||||
.execute([&key, &value])
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
MergeResult::Delete => {
|
||||
trx.prepare_cached(&format!(
|
||||
"DELETE FROM {} WHERE k = ?",
|
||||
table
|
||||
))
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?
|
||||
.execute([&key])
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
MergeResult::Skip => (),
|
||||
}
|
||||
}
|
||||
ValueOp::AtomicAdd(by) => {
|
||||
if *by >= 0 {
|
||||
trx.prepare_cached(&format!(
|
||||
concat!(
|
||||
"INSERT INTO {} (k, v) VALUES (?, ?) ",
|
||||
"ON CONFLICT(k) DO UPDATE SET v = v + excluded.v"
|
||||
),
|
||||
table
|
||||
))
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?
|
||||
.execute(params![&key, *by])
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?;
|
||||
} else {
|
||||
trx.prepare_cached(&format!(
|
||||
"UPDATE {table} SET v = v + ? WHERE k = ?"
|
||||
))
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?
|
||||
.execute(params![*by, &key])
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
}
|
||||
ValueOp::AddAndGet(by) => {
|
||||
result.push_counter_id(
|
||||
trx.prepare_cached(&format!(
|
||||
concat!(
|
||||
"INSERT INTO {} (k, v) VALUES (?, ?) ",
|
||||
"ON CONFLICT(k) DO UPDATE SET v = v + ",
|
||||
"excluded.v RETURNING v"
|
||||
),
|
||||
table
|
||||
))
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?
|
||||
.query_row(params![&key, &*by], |row| row.get::<_, i64>(0))
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?,
|
||||
);
|
||||
}
|
||||
ValueOp::Clear => {
|
||||
trx.prepare_cached(&format!("DELETE FROM {} WHERE k = ?", table))
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?
|
||||
.execute([&key])
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Operation::Index { field, key, set } => {
|
||||
let key = IndexKey {
|
||||
account_id,
|
||||
collection,
|
||||
document_id,
|
||||
field: *field,
|
||||
key: &*key,
|
||||
}
|
||||
.serialize(0);
|
||||
|
||||
if *set {
|
||||
trx.prepare_cached("INSERT OR IGNORE INTO i (k) VALUES (?)")
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?
|
||||
.execute([&key])
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?;
|
||||
} else {
|
||||
trx.prepare_cached("DELETE FROM i WHERE k = ?")
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?
|
||||
.execute([&key])
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
}
|
||||
Operation::Log { collection, set } => {
|
||||
let key = LogKey {
|
||||
account_id,
|
||||
collection: u8::from(*collection),
|
||||
change_id,
|
||||
}
|
||||
.serialize(0);
|
||||
|
||||
trx.prepare_cached("INSERT OR REPLACE INTO l (k, v) VALUES (?, ?)")
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?
|
||||
.execute([&key, set])
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
Operation::AssertValue {
|
||||
class,
|
||||
assert_value,
|
||||
} => {
|
||||
let key = class.serialize(account_id, collection, document_id, 0);
|
||||
let table = char::from(class.subspace(collection));
|
||||
|
||||
let matches = trx
|
||||
.prepare_cached(&format!("SELECT v FROM {} WHERE k = ?", table))
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?
|
||||
.query_row([&key], |row| {
|
||||
Ok(assert_value.matches(row.get_ref(0)?.as_bytes()?))
|
||||
})
|
||||
.optional()
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?
|
||||
.unwrap_or_else(|| assert_value.is_none());
|
||||
if !matches {
|
||||
trx.rollback()
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?;
|
||||
return Err(trc::StoreEvent::AssertValueFailed
|
||||
.into_err()
|
||||
.caused_by(trc::location!()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trx.commit().map(|_| result).map_err(into_error)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn purge_store(&self) -> trc::Result<()> {
|
||||
let manager = self.conn_pool.clone();
|
||||
self.spawn_worker(move || {
|
||||
let conn = manager.get().map_err(into_error)?;
|
||||
for subspace in [SUBSPACE_QUOTA, SUBSPACE_COUNTER, SUBSPACE_IN_MEMORY_COUNTER] {
|
||||
conn.prepare_cached(&format!("DELETE FROM {} WHERE v = 0", char::from(subspace),))
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?
|
||||
.execute([])
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_range(&self, from: impl Key, to: impl Key) -> trc::Result<()> {
|
||||
let manager = self.conn_pool.clone();
|
||||
self.spawn_worker(move || {
|
||||
let conn = manager.get().map_err(into_error)?;
|
||||
|
||||
conn.prepare_cached(&format!(
|
||||
"DELETE FROM {} WHERE k >= ? AND k < ?",
|
||||
char::from(from.subspace()),
|
||||
))
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?
|
||||
.execute([from.serialize(0), to.serialize(0)])
|
||||
.map_err(into_error)
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{BlobStore, backend::fs::FsStore, registry::bootstrap::Bootstrap};
|
||||
use registry::schema::{prelude::ObjectType, structs};
|
||||
|
||||
#[allow(unreachable_patterns)]
|
||||
impl BlobStore {
|
||||
pub async fn build(bp: &mut Bootstrap) -> Option<Self> {
|
||||
let result = match bp.setting_infallible::<structs::BlobStore>().await {
|
||||
structs::BlobStore::Default => return Some(BlobStore::Store(bp.data_store.clone())),
|
||||
#[cfg(feature = "foundation")]
|
||||
structs::BlobStore::FoundationDb(foundation_db_store) => {
|
||||
crate::backend::foundationdb::FdbStore::open(foundation_db_store)
|
||||
.await
|
||||
.map(BlobStore::Store)
|
||||
}
|
||||
#[cfg(feature = "postgres")]
|
||||
structs::BlobStore::PostgreSql(postgre_sql_store) => {
|
||||
crate::backend::postgres::PostgresStore::open(postgre_sql_store)
|
||||
.await
|
||||
.map(BlobStore::Store)
|
||||
}
|
||||
#[cfg(feature = "mysql")]
|
||||
structs::BlobStore::MySql(my_sql_store) => {
|
||||
crate::backend::mysql::MysqlStore::open(my_sql_store)
|
||||
.await
|
||||
.map(BlobStore::Store)
|
||||
}
|
||||
#[cfg(feature = "s3")]
|
||||
structs::BlobStore::S3(s3_store) => crate::backend::s3::S3Store::open(s3_store).await,
|
||||
#[cfg(feature = "azure")]
|
||||
structs::BlobStore::Azure(azure_store) => {
|
||||
crate::backend::azure::AzureStore::open(azure_store).await
|
||||
}
|
||||
structs::BlobStore::FileSystem(file_system_store) => {
|
||||
FsStore::open(file_system_store).await
|
||||
}
|
||||
_ => Err("Binary was not compiled with the selected blob store backend".to_string()),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(store) => Some(store),
|
||||
Err(err) => {
|
||||
bp.build_error(ObjectType::BlobStore.singleton(), err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
IterateParams, RegistryStore, RegistryStoreInner, Store, U16_LEN, U32_LEN, U64_LEN, ValueKey,
|
||||
write::{
|
||||
BatchBuilder, ValueClass,
|
||||
assert::AssertValue,
|
||||
key::{DeserializeBigEndian, KeySerializer},
|
||||
now,
|
||||
},
|
||||
};
|
||||
use registry::{
|
||||
schema::{enums::ClusterNodeStatus, structs::ClusterNode},
|
||||
types::datetime::UTCDateTime,
|
||||
};
|
||||
use std::time::Duration;
|
||||
use trc::AddContext;
|
||||
use utils::snowflake::MAX_NODE_ID;
|
||||
|
||||
const STALE_NODE_TIMEOUT: u64 = 60 * 60; // 1 hour
|
||||
const DEAD_NODE_TIMEOUT: u64 = 60 * 60 * 24; // 24 hours
|
||||
const MAX_LEASE_RETRIES: u32 = 5;
|
||||
|
||||
struct NodeSlot {
|
||||
node_id: u16,
|
||||
hostname: String,
|
||||
last_renewal: u64,
|
||||
elapsed: u64,
|
||||
hash: u64,
|
||||
}
|
||||
|
||||
struct NodeClaim {
|
||||
node_id: u16,
|
||||
assert: AssertValue,
|
||||
}
|
||||
|
||||
impl RegistryStoreInner {
|
||||
pub(super) async fn acquire_node_id(&mut self) -> Result<(), String> {
|
||||
let mut retry_count = 0;
|
||||
let slots = loop {
|
||||
let now = now();
|
||||
let slots = NodeSlot::list(&self.store, now)
|
||||
.await
|
||||
.map_err(|err| format!("Failed to iterate store: {err}"))?;
|
||||
let claim = NodeSlot::claim(&slots, &self.env_hostname)?;
|
||||
let mut batch = BatchBuilder::new();
|
||||
|
||||
batch
|
||||
.assert_value(ValueClass::NodeId(claim.node_id), claim.assert)
|
||||
.set(
|
||||
ValueClass::NodeId(claim.node_id),
|
||||
KeySerializer::new(self.env_hostname.len() + U64_LEN)
|
||||
.write(now)
|
||||
.write(&self.env_hostname)
|
||||
.finalize(),
|
||||
);
|
||||
|
||||
match self.store.write(batch.build_all()).await {
|
||||
Ok(_) => {
|
||||
self.node_id = claim.node_id;
|
||||
break slots;
|
||||
}
|
||||
Err(err) => {
|
||||
if err.is_assertion_failure() && retry_count < MAX_LEASE_RETRIES {
|
||||
retry_count += 1;
|
||||
continue;
|
||||
} else {
|
||||
return Err(format!("Failed to write node id to store: {err}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = NodeSlot::release(
|
||||
&self.store,
|
||||
slots
|
||||
.iter()
|
||||
.filter(|slot| slot.node_id != self.node_id && slot.is_dead()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
trc::error!(err.details("Failed to release expired node id leases"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryStore {
|
||||
pub fn node_id(&self) -> u16 {
|
||||
self.0.node_id
|
||||
}
|
||||
|
||||
pub fn refresh_node_id_interval(&self) -> Duration {
|
||||
Duration::from_secs(STALE_NODE_TIMEOUT / 2)
|
||||
}
|
||||
|
||||
pub async fn cluster_node_list(&self) -> trc::Result<Vec<ClusterNode>> {
|
||||
NodeSlot::list(&self.0.store, now())
|
||||
.await
|
||||
.map(|slots| slots.into_iter().map(ClusterNode::from).collect())
|
||||
}
|
||||
|
||||
pub async fn refresh_node_id_lease(&self) -> trc::Result<()> {
|
||||
let node_id = self.0.node_id;
|
||||
let assert = match NodeSlot::list(&self.0.store, now())
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.into_iter()
|
||||
.find(|slot| slot.node_id == node_id)
|
||||
{
|
||||
Some(slot) if slot.is_owned_by(&self.0.env_hostname) => AssertValue::Hash(slot.hash),
|
||||
Some(slot) => {
|
||||
return Err(trc::StoreEvent::AssertValueFailed
|
||||
.into_err()
|
||||
.details("Node id lease is held by another host")
|
||||
.ctx(trc::Key::Id, node_id)
|
||||
.ctx(trc::Key::Hostname, slot.hostname));
|
||||
}
|
||||
None => AssertValue::None,
|
||||
};
|
||||
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch.assert_value(ValueClass::NodeId(node_id), assert).set(
|
||||
ValueClass::NodeId(node_id),
|
||||
KeySerializer::new(self.0.env_hostname.len() + U64_LEN)
|
||||
.write(now())
|
||||
.write(&self.0.env_hostname)
|
||||
.finalize(),
|
||||
);
|
||||
|
||||
self.0
|
||||
.store
|
||||
.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub async fn purge_dead_nodes(&self) -> trc::Result<()> {
|
||||
let node_id = self.0.node_id;
|
||||
let slots = NodeSlot::list(&self.0.store, now())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if !slots.iter().any(|slot| {
|
||||
slot.node_id == node_id && slot.is_owned_by(&self.0.env_hostname) && !slot.is_stale()
|
||||
}) {
|
||||
Ok(())
|
||||
} else {
|
||||
NodeSlot::release(
|
||||
&self.0.store,
|
||||
slots
|
||||
.iter()
|
||||
.filter(|slot| slot.node_id != node_id && slot.is_dead()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeSlot {
|
||||
async fn list(store: &Store, now: u64) -> trc::Result<Vec<NodeSlot>> {
|
||||
let mut slots = Vec::new();
|
||||
|
||||
store
|
||||
.iterate(
|
||||
IterateParams::new(
|
||||
ValueKey::from(ValueClass::NodeId(0)),
|
||||
ValueKey::from(ValueClass::NodeId(u16::MAX)),
|
||||
)
|
||||
.ascending(),
|
||||
|key, value| {
|
||||
if key.len() == U16_LEN * 3 {
|
||||
let node_id = key.deserialize_be_u16(U32_LEN)?;
|
||||
|
||||
match (
|
||||
value.deserialize_be_u64(0),
|
||||
value
|
||||
.get(U64_LEN..)
|
||||
.and_then(|bytes| std::str::from_utf8(bytes).ok())
|
||||
.filter(|text| !text.is_empty()),
|
||||
) {
|
||||
(Ok(last_renewal), Some(hostname)) => {
|
||||
slots.push(NodeSlot {
|
||||
node_id,
|
||||
hostname: hostname.to_string(),
|
||||
last_renewal,
|
||||
elapsed: now.saturating_sub(last_renewal),
|
||||
hash: xxhash_rust::xxh3::xxh3_64(value),
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
trc::error!(
|
||||
trc::StoreEvent::DataCorruption
|
||||
.into_err()
|
||||
.details("Invalid node id lease")
|
||||
.ctx(trc::Key::Id, node_id)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map(|_| slots)
|
||||
}
|
||||
|
||||
fn claim(slots: &[NodeSlot], hostname: &str) -> Result<NodeClaim, String> {
|
||||
if let Some(slot) = slots
|
||||
.iter()
|
||||
.find(|slot| slot.is_owned_by(hostname) && slot.is_assignable())
|
||||
.or_else(|| {
|
||||
slots
|
||||
.iter()
|
||||
.find(|slot| slot.is_stale() && slot.is_assignable())
|
||||
})
|
||||
{
|
||||
return Ok(NodeClaim {
|
||||
node_id: slot.node_id,
|
||||
assert: AssertValue::Hash(slot.hash),
|
||||
});
|
||||
}
|
||||
|
||||
let mut leased = slots
|
||||
.iter()
|
||||
.filter(|slot| !slot.is_stale())
|
||||
.map(|slot| slot.node_id)
|
||||
.collect::<Vec<_>>();
|
||||
leased.sort_unstable();
|
||||
|
||||
let mut node_id = 0;
|
||||
for leased_id in leased {
|
||||
if leased_id > node_id {
|
||||
break;
|
||||
}
|
||||
node_id = leased_id.saturating_add(1);
|
||||
if node_id > MAX_NODE_ID {
|
||||
return Err(format!(
|
||||
"Failed to obtain a node id: all {} ids are leased by active nodes",
|
||||
MAX_NODE_ID as u32 + 1
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(NodeClaim {
|
||||
node_id,
|
||||
assert: AssertValue::None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn release<'x>(
|
||||
store: &Store,
|
||||
slots: impl Iterator<Item = &'x NodeSlot>,
|
||||
) -> trc::Result<()> {
|
||||
for slot in slots {
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
.assert_value(
|
||||
ValueClass::NodeId(slot.node_id),
|
||||
AssertValue::Hash(slot.hash),
|
||||
)
|
||||
.clear(ValueClass::NodeId(slot.node_id));
|
||||
|
||||
if let Err(err) = store.write(batch.build_all()).await
|
||||
&& !err.is_assertion_failure()
|
||||
{
|
||||
return Err(err.caused_by(trc::location!()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_owned_by(&self, hostname: &str) -> bool {
|
||||
self.hostname == hostname
|
||||
}
|
||||
|
||||
fn is_stale(&self) -> bool {
|
||||
self.elapsed > STALE_NODE_TIMEOUT
|
||||
}
|
||||
|
||||
fn is_dead(&self) -> bool {
|
||||
self.elapsed > DEAD_NODE_TIMEOUT
|
||||
}
|
||||
|
||||
fn is_assignable(&self) -> bool {
|
||||
self.node_id <= MAX_NODE_ID
|
||||
}
|
||||
|
||||
fn status(&self) -> ClusterNodeStatus {
|
||||
if self.is_dead() {
|
||||
ClusterNodeStatus::Inactive
|
||||
} else if self.is_stale() {
|
||||
ClusterNodeStatus::Stale
|
||||
} else {
|
||||
ClusterNodeStatus::Active
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NodeSlot> for ClusterNode {
|
||||
fn from(slot: NodeSlot) -> Self {
|
||||
ClusterNode {
|
||||
status: slot.status(),
|
||||
last_renewal: UTCDateTime::from_timestamp(slot.last_renewal.cast_signed()),
|
||||
node_id: slot.node_id as u64,
|
||||
hostname: slot.hostname,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{Store, registry::bootstrap::Bootstrap};
|
||||
use registry::schema::{
|
||||
prelude::ObjectType,
|
||||
structs::{DataStore, MetricsStore, TracingStore},
|
||||
};
|
||||
|
||||
#[allow(unreachable_patterns)]
|
||||
impl Store {
|
||||
pub async fn build(config: DataStore) -> Result<Self, String> {
|
||||
#[allow(unreachable_patterns)]
|
||||
match config {
|
||||
#[cfg(feature = "rocks")]
|
||||
DataStore::RocksDb(store) => crate::backend::rocksdb::RocksDbStore::open(store).await,
|
||||
#[cfg(feature = "foundation")]
|
||||
DataStore::FoundationDb(store) => {
|
||||
crate::backend::foundationdb::FdbStore::open(store).await
|
||||
}
|
||||
#[cfg(feature = "postgres")]
|
||||
DataStore::PostgreSql(store) => {
|
||||
crate::backend::postgres::PostgresStore::open(store).await
|
||||
}
|
||||
#[cfg(feature = "mysql")]
|
||||
DataStore::MySql(store) => crate::backend::mysql::MysqlStore::open(store).await,
|
||||
#[cfg(feature = "sqlite")]
|
||||
DataStore::Sqlite(store) => crate::backend::sqlite::SqliteStore::open(store),
|
||||
_ => Err("Binary was not compiled with the selected data store backend".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build_tracing(bp: &mut Bootstrap) -> Option<Self> {
|
||||
let result = match bp.setting_infallible::<TracingStore>().await {
|
||||
TracingStore::Disabled => Ok(None),
|
||||
TracingStore::Default => Ok(Some(bp.data_store.clone())),
|
||||
#[cfg(feature = "foundation")]
|
||||
TracingStore::FoundationDb(store) => {
|
||||
crate::backend::foundationdb::FdbStore::open(store)
|
||||
.await
|
||||
.map(Some)
|
||||
}
|
||||
#[cfg(feature = "postgres")]
|
||||
TracingStore::PostgreSql(store) => crate::backend::postgres::PostgresStore::open(store)
|
||||
.await
|
||||
.map(Some),
|
||||
#[cfg(feature = "mysql")]
|
||||
TracingStore::MySql(store) => crate::backend::mysql::MysqlStore::open(store)
|
||||
.await
|
||||
.map(Some),
|
||||
_ => Err("Binary was not compiled with the selected tracing store backend".to_string()),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(store) => store,
|
||||
Err(err) => {
|
||||
bp.build_warning(ObjectType::TracingStore.singleton(), err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build_metrics(bp: &mut Bootstrap) -> Option<Self> {
|
||||
let result = match bp.setting_infallible::<MetricsStore>().await {
|
||||
MetricsStore::Disabled => Ok(None),
|
||||
MetricsStore::Default => Ok(Some(bp.data_store.clone())),
|
||||
#[cfg(feature = "foundation")]
|
||||
MetricsStore::FoundationDb(store) => {
|
||||
crate::backend::foundationdb::FdbStore::open(store)
|
||||
.await
|
||||
.map(Some)
|
||||
}
|
||||
#[cfg(feature = "postgres")]
|
||||
MetricsStore::PostgreSql(store) => crate::backend::postgres::PostgresStore::open(store)
|
||||
.await
|
||||
.map(Some),
|
||||
#[cfg(feature = "mysql")]
|
||||
MetricsStore::MySql(store) => crate::backend::mysql::MysqlStore::open(store)
|
||||
.await
|
||||
.map(Some),
|
||||
_ => Err("Binary was not compiled with the selected metrics store backend".to_string()),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(store) => store,
|
||||
Err(err) => {
|
||||
bp.build_warning(ObjectType::MetricsStore.singleton(), err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{LookupStores, registry::bootstrap::Bootstrap};
|
||||
use registry::schema::structs::{LookupStore, StoreLookup};
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
impl LookupStores {
|
||||
pub async fn build(bp: &mut Bootstrap) -> Self {
|
||||
let mut stores = LookupStores::default();
|
||||
stores.parse_stores(bp).await;
|
||||
stores.parse_static(bp).await;
|
||||
stores.parse_http(bp).await;
|
||||
stores
|
||||
}
|
||||
|
||||
#[allow(unreachable_patterns)]
|
||||
pub async fn parse_stores(&mut self, bp: &mut Bootstrap) {
|
||||
for store in bp.list_infallible::<StoreLookup>().await {
|
||||
let id = store.id;
|
||||
let store = store.object;
|
||||
|
||||
let result = match store.store {
|
||||
#[cfg(feature = "postgres")]
|
||||
LookupStore::PostgreSql(postgre_sql_store) => {
|
||||
crate::backend::postgres::PostgresStore::open(postgre_sql_store)
|
||||
.await
|
||||
.map(crate::InMemoryStore::Store)
|
||||
}
|
||||
#[cfg(feature = "mysql")]
|
||||
LookupStore::MySql(my_sql_store) => {
|
||||
crate::backend::mysql::MysqlStore::open(my_sql_store)
|
||||
.await
|
||||
.map(crate::InMemoryStore::Store)
|
||||
}
|
||||
#[cfg(feature = "sqlite")]
|
||||
LookupStore::Sqlite(sqlite_store) => {
|
||||
crate::backend::sqlite::SqliteStore::open(sqlite_store)
|
||||
.map(crate::InMemoryStore::Store)
|
||||
}
|
||||
#[cfg(feature = "redis")]
|
||||
LookupStore::Redis(redis_store) => {
|
||||
crate::backend::redis::RedisStore::open_single(redis_store).await
|
||||
}
|
||||
#[cfg(feature = "redis")]
|
||||
LookupStore::RedisCluster(redis_cluster_store) => {
|
||||
crate::backend::redis::RedisStore::open_cluster(redis_cluster_store).await
|
||||
}
|
||||
_ => Err(
|
||||
"Binary was not compiled with the selected lookup store backend".to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(lookup) => match self.stores.entry(store.namespace.as_str().into()) {
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(lookup);
|
||||
}
|
||||
Entry::Occupied(_) => {
|
||||
bp.build_error(
|
||||
id,
|
||||
format!(
|
||||
"A lookup store with the {} namespace already exists",
|
||||
store.namespace
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
bp.build_error(id, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{InMemoryStore, registry::bootstrap::Bootstrap};
|
||||
use registry::schema::{prelude::ObjectType, structs};
|
||||
|
||||
#[allow(unreachable_patterns)]
|
||||
impl InMemoryStore {
|
||||
pub async fn build(bp: &mut Bootstrap) -> Option<Self> {
|
||||
let result = match bp.setting_infallible::<structs::InMemoryStore>().await {
|
||||
structs::InMemoryStore::Default => {
|
||||
return Some(InMemoryStore::Store(bp.data_store.clone()));
|
||||
}
|
||||
#[cfg(feature = "redis")]
|
||||
structs::InMemoryStore::Redis(redis_store) => {
|
||||
crate::backend::redis::RedisStore::open_single(redis_store).await
|
||||
}
|
||||
#[cfg(feature = "redis")]
|
||||
structs::InMemoryStore::RedisCluster(redis_cluster_store) => {
|
||||
crate::backend::redis::RedisStore::open_cluster(redis_cluster_store).await
|
||||
}
|
||||
#[cfg(feature = "redis")]
|
||||
structs::InMemoryStore::RedisSentinel(redis_sentinel_store) => {
|
||||
crate::backend::redis::RedisStore::open_sentinel(redis_sentinel_store).await
|
||||
}
|
||||
_ => Err("Binary was not compiled with the selected in-memory backend".to_string()),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(store) => Some(store),
|
||||
Err(err) => {
|
||||
bp.build_error(ObjectType::InMemoryStore.singleton(), err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod blob;
|
||||
pub mod cluster;
|
||||
pub mod data;
|
||||
pub mod lookup;
|
||||
pub mod memory;
|
||||
pub mod registry;
|
||||
pub mod search;
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
RegistryStore, RegistryStoreInner, Store, backend::ephemeral::EphemeralStore,
|
||||
registry::local::RegistryInit,
|
||||
};
|
||||
use rand::{RngExt, distr::Alphanumeric, rng};
|
||||
use std::path::PathBuf;
|
||||
|
||||
impl RegistryStore {
|
||||
pub async fn init(local: PathBuf, acquire_node_id: bool) -> Result<Self, String> {
|
||||
// Create inner store
|
||||
let mut inner = RegistryStoreInner::new(local);
|
||||
|
||||
// Build store
|
||||
inner.store = match inner.read_data_store().await {
|
||||
RegistryInit::Ok(data_store) => Store::build(data_store).await?,
|
||||
RegistryInit::Err(err) => return Err(err),
|
||||
RegistryInit::Bootstrap => {
|
||||
inner.env_recovery_mode = true;
|
||||
|
||||
if inner.env_recovery_admin.is_none() {
|
||||
let password = rng()
|
||||
.sample_iter(Alphanumeric)
|
||||
.take(16)
|
||||
.map(char::from)
|
||||
.collect::<String>();
|
||||
eprintln!();
|
||||
eprintln!("════════════════════════════════════════════════════════════");
|
||||
eprintln!("🔑 Stalwart bootstrap mode - temporary administrator account");
|
||||
eprintln!();
|
||||
eprintln!(" username: admin");
|
||||
eprintln!(" password: {password}");
|
||||
eprintln!();
|
||||
eprintln!("Use these credentials to complete the initial setup at the");
|
||||
eprintln!("/admin web UI. Once setup is done, Stalwart will provision a");
|
||||
eprintln!("permanent administrator and this temporary account will no");
|
||||
eprintln!("longer apply.");
|
||||
eprintln!();
|
||||
eprintln!("This password is shown only once. To pin a credential");
|
||||
eprintln!("instead, set STALWART_RECOVERY_ADMIN=admin:<password> in the");
|
||||
eprintln!("env file.");
|
||||
eprintln!("════════════════════════════════════════════════════════════");
|
||||
eprintln!();
|
||||
inner.env_recovery_admin = Some(("admin".to_string(), password));
|
||||
}
|
||||
|
||||
EphemeralStore::open()
|
||||
}
|
||||
};
|
||||
|
||||
Self::from_inner(inner, acquire_node_id).await
|
||||
}
|
||||
|
||||
pub fn from_inner_bootstrapped(inner: RegistryStoreInner) -> Self {
|
||||
Self(inner.into())
|
||||
}
|
||||
|
||||
pub async fn from_inner(
|
||||
mut inner: RegistryStoreInner,
|
||||
acquire_node_id: bool,
|
||||
) -> Result<Self, String> {
|
||||
// Create tables (SQL only)
|
||||
inner
|
||||
.store
|
||||
.create_tables()
|
||||
.await
|
||||
.map_err(|err| format!("Failed to create tables: {err}"))?;
|
||||
|
||||
if acquire_node_id {
|
||||
inner.acquire_node_id().await?;
|
||||
}
|
||||
|
||||
Ok(Self(inner.into()))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn recovery_admin(&self) -> Option<&(String, String)> {
|
||||
self.0.env_recovery_admin.as_ref()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn cluster_role(&self) -> Option<&str> {
|
||||
self.0.env_cluster_role.as_deref()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn cluster_push_shard(&self) -> u32 {
|
||||
self.0.env_push_shard_id
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn local_hostname(&self) -> &str {
|
||||
&self.0.env_hostname
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn public_url(&self) -> Option<&str> {
|
||||
self.0.env_public_url.as_deref()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_recovery_mode(&self) -> bool {
|
||||
self.0.env_recovery_mode
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_bootstrap_mode(&self) -> bool {
|
||||
self.0.store.is_ephemeral()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn path(&self) -> &PathBuf {
|
||||
&self.0.local_path
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn store(&self) -> &Store {
|
||||
&self.0.store
|
||||
}
|
||||
|
||||
pub fn initialize_inner(&self, store: Store) -> RegistryStoreInner {
|
||||
let mut inner = self.0.as_ref().clone();
|
||||
inner.store = store;
|
||||
inner
|
||||
}
|
||||
|
||||
#[cfg(feature = "test_mode")]
|
||||
pub fn clone_with_public_url(&self, url: String) -> Self {
|
||||
let mut inner = self.0.as_ref().clone();
|
||||
inner.env_public_url = Some(url);
|
||||
Self(inner.into())
|
||||
}
|
||||
|
||||
#[cfg(feature = "test_mode")]
|
||||
pub async fn new(
|
||||
path: &str,
|
||||
store: Store,
|
||||
hostname: String,
|
||||
push_shard_id: u32,
|
||||
cluster_role: Option<String>,
|
||||
) -> Self {
|
||||
Self::from_inner(
|
||||
RegistryStoreInner {
|
||||
local_path: PathBuf::from(path),
|
||||
store,
|
||||
node_id: 0,
|
||||
env_recovery_mode: false,
|
||||
env_recovery_admin: Some(("admin".to_string(), "popolna_zapora".to_string())),
|
||||
env_cluster_role: cluster_role,
|
||||
env_push_shard_id: push_shard_id,
|
||||
env_hostname: hostname,
|
||||
env_public_url: None,
|
||||
id_generator: utils::snowflake::SnowflakeIdGenerator::new(),
|
||||
},
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
SearchStore,
|
||||
backend::{elastic::ElasticSearchStore, meili::MeiliSearchStore},
|
||||
registry::bootstrap::Bootstrap,
|
||||
};
|
||||
use registry::schema::{prelude::ObjectType, structs};
|
||||
|
||||
#[allow(unreachable_patterns)]
|
||||
impl SearchStore {
|
||||
pub async fn build(bp: &mut Bootstrap) -> Option<Self> {
|
||||
let result = match bp.setting_infallible::<structs::SearchStore>().await {
|
||||
structs::SearchStore::Default => {
|
||||
return Some(SearchStore::Store(bp.data_store.clone()));
|
||||
}
|
||||
structs::SearchStore::ElasticSearch(elastic_search_store) => {
|
||||
ElasticSearchStore::open(elastic_search_store).await
|
||||
}
|
||||
structs::SearchStore::Meilisearch(meilisearch_store) => {
|
||||
MeiliSearchStore::open(meilisearch_store).await
|
||||
}
|
||||
#[cfg(feature = "foundation")]
|
||||
structs::SearchStore::FoundationDb(foundation_db_store) => {
|
||||
crate::backend::foundationdb::FdbStore::open(foundation_db_store)
|
||||
.await
|
||||
.map(SearchStore::Store)
|
||||
}
|
||||
#[cfg(feature = "postgres")]
|
||||
structs::SearchStore::PostgreSql(postgre_sql_store) => {
|
||||
crate::backend::postgres::PostgresStore::open(postgre_sql_store)
|
||||
.await
|
||||
.map(SearchStore::Store)
|
||||
}
|
||||
#[cfg(feature = "mysql")]
|
||||
structs::SearchStore::MySql(my_sql_store) => {
|
||||
crate::backend::mysql::MysqlStore::open(my_sql_store)
|
||||
.await
|
||||
.map(SearchStore::Store)
|
||||
}
|
||||
_ => Err("Binary was not compiled with the selected search store backend".to_string()),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(store) => Some(store),
|
||||
Err(err) => {
|
||||
bp.build_error(ObjectType::SearchStore.singleton(), err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{BlobStore, CompressionAlgo, Store, U32_LEN};
|
||||
use std::{ops::Range, time::Instant};
|
||||
use trc::{AddContext, StoreEvent};
|
||||
|
||||
const MAGIC_MARKER: u8 = 0xa0;
|
||||
const LZ4_MARKER: u8 = MAGIC_MARKER | 0x01;
|
||||
//const ZSTD_MARKER: u8 = MAGIC_MARKER | 0x02;
|
||||
const NONE_MARKER: u8 = 0x00;
|
||||
|
||||
impl BlobStore {
|
||||
pub async fn get_blob(&self, key: &[u8], range: Range<usize>) -> trc::Result<Option<Vec<u8>>> {
|
||||
let start_time = Instant::now();
|
||||
let result = match &self {
|
||||
BlobStore::Store(store) => match store {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Store::SQLite(store) => store.get_blob(key, 0..usize::MAX).await,
|
||||
#[cfg(feature = "foundation")]
|
||||
Store::FoundationDb(store) => store.get_blob(key, 0..usize::MAX).await,
|
||||
#[cfg(feature = "postgres")]
|
||||
Store::PostgreSQL(store) => store.get_blob(key, 0..usize::MAX).await,
|
||||
#[cfg(feature = "mysql")]
|
||||
Store::MySQL(store) => store.get_blob(key, 0..usize::MAX).await,
|
||||
#[cfg(feature = "rocks")]
|
||||
Store::RocksDb(store) => store.get_blob(key, 0..usize::MAX).await,
|
||||
Store::Ephemeral(store) => store.get_blob(key, 0..usize::MAX).await,
|
||||
Store::None => Err(trc::StoreEvent::NotConfigured.into()),
|
||||
},
|
||||
BlobStore::Fs(store) => store.get_blob(key, 0..usize::MAX).await,
|
||||
#[cfg(feature = "s3")]
|
||||
BlobStore::S3(store) => store.get_blob(key, 0..usize::MAX).await,
|
||||
#[cfg(feature = "azure")]
|
||||
BlobStore::Azure(store) => store.get_blob(key, 0..usize::MAX).await,
|
||||
}
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
trc::event!(
|
||||
Store(StoreEvent::BlobRead),
|
||||
Key = key,
|
||||
Elapsed = start_time.elapsed(),
|
||||
Size = result.as_ref().map_or(0, |data| data.len()),
|
||||
);
|
||||
|
||||
let Some(mut data) = result else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut data = match data.last().copied() {
|
||||
Some(LZ4_MARKER) => {
|
||||
lz4_flex::decompress_size_prepended(data.get(..data.len() - 1).unwrap_or_default())
|
||||
.map_err(|err| {
|
||||
trc::StoreEvent::DecompressError
|
||||
.reason(err)
|
||||
.ctx(trc::Key::Key, key)
|
||||
.ctx(trc::Key::CausedBy, trc::location!())
|
||||
})?
|
||||
}
|
||||
Some(NONE_MARKER) => {
|
||||
if !data.is_empty() {
|
||||
data.truncate(data.len() - 1);
|
||||
}
|
||||
data
|
||||
}
|
||||
Some(_) => {
|
||||
trc::event!(Store(StoreEvent::BlobMissingMarker), Key = key);
|
||||
|
||||
data
|
||||
}
|
||||
None => {
|
||||
return Ok(Some(data));
|
||||
}
|
||||
};
|
||||
|
||||
if range.start == 0 {
|
||||
if range.end > data.len() {
|
||||
Ok(Some(data))
|
||||
} else {
|
||||
data.truncate(range.end);
|
||||
Ok(Some(data))
|
||||
}
|
||||
} else {
|
||||
Ok(Some(
|
||||
data.get(range.start..range.end)
|
||||
.unwrap_or_default()
|
||||
.to_vec(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn put_blob(
|
||||
&self,
|
||||
key: &[u8],
|
||||
data: &[u8],
|
||||
compression: CompressionAlgo,
|
||||
) -> trc::Result<()> {
|
||||
let data = match compression {
|
||||
CompressionAlgo::None => {
|
||||
let mut uncompressed = Vec::with_capacity(data.len() + 1);
|
||||
uncompressed.extend_from_slice(data);
|
||||
uncompressed.push(NONE_MARKER);
|
||||
uncompressed
|
||||
}
|
||||
CompressionAlgo::Lz4 => {
|
||||
let mut compressed =
|
||||
vec![
|
||||
LZ4_MARKER;
|
||||
lz4_flex::block::get_maximum_output_size(data.len()) + U32_LEN + 1
|
||||
];
|
||||
|
||||
// Compress the data
|
||||
let compressed_len =
|
||||
lz4_flex::compress_into(data, &mut compressed[U32_LEN..]).unwrap();
|
||||
|
||||
// Prepend the length of the uncompressed data
|
||||
compressed[..U32_LEN].copy_from_slice(&(data.len() as u32).to_le_bytes());
|
||||
|
||||
// Truncate to the actual size
|
||||
compressed.truncate(compressed_len + U32_LEN + 1);
|
||||
compressed
|
||||
}
|
||||
};
|
||||
|
||||
let start_time = Instant::now();
|
||||
let result = match &self {
|
||||
BlobStore::Store(store) => match store {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Store::SQLite(store) => store.put_blob(key, &data).await,
|
||||
#[cfg(feature = "foundation")]
|
||||
Store::FoundationDb(store) => store.put_blob(key, &data).await,
|
||||
#[cfg(feature = "postgres")]
|
||||
Store::PostgreSQL(store) => store.put_blob(key, &data).await,
|
||||
#[cfg(feature = "mysql")]
|
||||
Store::MySQL(store) => store.put_blob(key, &data).await,
|
||||
#[cfg(feature = "rocks")]
|
||||
Store::RocksDb(store) => store.put_blob(key, &data).await,
|
||||
Store::Ephemeral(store) => store.put_blob(key, &data).await,
|
||||
Store::None => Err(trc::StoreEvent::NotConfigured.into()),
|
||||
},
|
||||
BlobStore::Fs(store) => store.put_blob(key, &data).await,
|
||||
#[cfg(feature = "s3")]
|
||||
BlobStore::S3(store) => store.put_blob(key, &data).await,
|
||||
#[cfg(feature = "azure")]
|
||||
BlobStore::Azure(store) => store.put_blob(key, &data).await,
|
||||
}
|
||||
.caused_by(trc::location!());
|
||||
|
||||
trc::event!(
|
||||
Store(StoreEvent::BlobWrite),
|
||||
Key = key,
|
||||
Elapsed = start_time.elapsed(),
|
||||
Size = data.len(),
|
||||
);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub async fn delete_blob(&self, key: &[u8]) -> trc::Result<bool> {
|
||||
let start_time = Instant::now();
|
||||
let result = match &self {
|
||||
BlobStore::Store(store) => match store {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Store::SQLite(store) => store.delete_blob(key).await,
|
||||
#[cfg(feature = "foundation")]
|
||||
Store::FoundationDb(store) => store.delete_blob(key).await,
|
||||
#[cfg(feature = "postgres")]
|
||||
Store::PostgreSQL(store) => store.delete_blob(key).await,
|
||||
#[cfg(feature = "mysql")]
|
||||
Store::MySQL(store) => store.delete_blob(key).await,
|
||||
#[cfg(feature = "rocks")]
|
||||
Store::RocksDb(store) => store.delete_blob(key).await,
|
||||
Store::Ephemeral(store) => store.delete_blob(key).await,
|
||||
Store::None => Err(trc::StoreEvent::NotConfigured.into()),
|
||||
},
|
||||
BlobStore::Fs(store) => store.delete_blob(key).await,
|
||||
#[cfg(feature = "s3")]
|
||||
BlobStore::S3(store) => store.delete_blob(key).await,
|
||||
#[cfg(feature = "azure")]
|
||||
BlobStore::Azure(store) => store.delete_blob(key).await,
|
||||
}
|
||||
.caused_by(trc::location!());
|
||||
|
||||
trc::event!(
|
||||
Store(StoreEvent::BlobWrite),
|
||||
Key = key,
|
||||
Elapsed = start_time.elapsed(),
|
||||
);
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use registry::schema::structs::Rate;
|
||||
use std::borrow::Cow;
|
||||
use trc::AddContext;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crate::{
|
||||
Deserialize, InMemoryStore, IterateParams, QueryResult, Store, U64_LEN, Value, ValueKey,
|
||||
write::{
|
||||
BatchBuilder, Operation, ValueClass, ValueOp,
|
||||
key::{DeserializeBigEndian, KeySerializer},
|
||||
now,
|
||||
},
|
||||
};
|
||||
use crate::{
|
||||
SerializeInfallible,
|
||||
backend::{http::lookup::HttpStoreGet, memory::StaticMemoryStore},
|
||||
write::{InMemoryClass, assert::AssertValue},
|
||||
};
|
||||
|
||||
pub struct KeyValue<T> {
|
||||
pub key: Vec<u8>,
|
||||
pub value: T,
|
||||
pub expires: Option<u64>,
|
||||
}
|
||||
|
||||
impl InMemoryStore {
|
||||
pub async fn key_set(&self, kv: KeyValue<Vec<u8>>) -> trc::Result<()> {
|
||||
match self {
|
||||
InMemoryStore::Store(store) => {
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch.any_op(Operation::Value {
|
||||
class: ValueClass::InMemory(InMemoryClass::Key(kv.key)),
|
||||
op: ValueOp::Set(
|
||||
KeySerializer::new(kv.value.len() + U64_LEN)
|
||||
.write(kv.expires.map_or(u64::MAX, |expires| now() + expires))
|
||||
.write(kv.value.as_slice())
|
||||
.finalize(),
|
||||
),
|
||||
});
|
||||
store.write(batch.build_all()).await.map(|_| ())
|
||||
}
|
||||
#[cfg(feature = "redis")]
|
||||
InMemoryStore::Redis(store) => store.key_set(&kv.key, &kv.value, kv.expires).await,
|
||||
InMemoryStore::Static(_) | InMemoryStore::Http(_) => {
|
||||
Err(trc::StoreEvent::NotSupported.into_err())
|
||||
}
|
||||
}
|
||||
.caused_by(trc::location!())
|
||||
}
|
||||
|
||||
pub async fn counter_incr(&self, kv: KeyValue<i64>, return_value: bool) -> trc::Result<i64> {
|
||||
match self {
|
||||
InMemoryStore::Store(store) => {
|
||||
let mut batch = BatchBuilder::new();
|
||||
|
||||
if let Some(expires) = kv.expires {
|
||||
batch.any_op(Operation::Value {
|
||||
class: ValueClass::InMemory(InMemoryClass::Key(kv.key.clone())),
|
||||
op: ValueOp::Set(
|
||||
KeySerializer::new(U64_LEN * 2)
|
||||
.write(0u64)
|
||||
.write(now() + expires)
|
||||
.finalize(),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if return_value {
|
||||
batch.any_op(Operation::Value {
|
||||
class: ValueClass::InMemory(InMemoryClass::Counter(kv.key)),
|
||||
op: ValueOp::AddAndGet(kv.value),
|
||||
});
|
||||
|
||||
store
|
||||
.write(batch.build_all())
|
||||
.await
|
||||
.and_then(|r| r.last_counter_id())
|
||||
} else {
|
||||
batch.any_op(Operation::Value {
|
||||
class: ValueClass::InMemory(InMemoryClass::Counter(kv.key)),
|
||||
op: ValueOp::AtomicAdd(kv.value),
|
||||
});
|
||||
|
||||
store.write(batch.build_all()).await.map(|_| 0)
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "redis")]
|
||||
InMemoryStore::Redis(store) => store.key_incr(&kv.key, kv.value, kv.expires).await,
|
||||
InMemoryStore::Static(_) | InMemoryStore::Http(_) => {
|
||||
Err(trc::StoreEvent::NotSupported.into_err())
|
||||
}
|
||||
}
|
||||
.caused_by(trc::location!())
|
||||
}
|
||||
|
||||
pub async fn key_delete(&self, key: impl Into<LookupKey<'_>>) -> trc::Result<()> {
|
||||
match self {
|
||||
InMemoryStore::Store(store) => {
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch.any_op(Operation::Value {
|
||||
class: ValueClass::InMemory(InMemoryClass::Key(key.into().into_bytes())),
|
||||
op: ValueOp::Clear,
|
||||
});
|
||||
store.write(batch.build_all()).await.map(|_| ())
|
||||
}
|
||||
#[cfg(feature = "redis")]
|
||||
InMemoryStore::Redis(store) => store.key_delete(key.into().as_bytes()).await,
|
||||
InMemoryStore::Static(_) | InMemoryStore::Http(_) => {
|
||||
Err(trc::StoreEvent::NotSupported.into_err())
|
||||
}
|
||||
}
|
||||
.caused_by(trc::location!())
|
||||
}
|
||||
|
||||
pub async fn counter_delete(&self, key: impl Into<LookupKey<'_>>) -> trc::Result<()> {
|
||||
match self {
|
||||
InMemoryStore::Store(store) => {
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch.any_op(Operation::Value {
|
||||
class: ValueClass::InMemory(InMemoryClass::Counter(key.into().into_bytes())),
|
||||
op: ValueOp::Clear,
|
||||
});
|
||||
store.write(batch.build_all()).await.map(|_| ())
|
||||
}
|
||||
#[cfg(feature = "redis")]
|
||||
InMemoryStore::Redis(store) => store.key_delete(key.into().as_bytes()).await,
|
||||
InMemoryStore::Static(_) | InMemoryStore::Http(_) => {
|
||||
Err(trc::StoreEvent::NotSupported.into_err())
|
||||
}
|
||||
}
|
||||
.caused_by(trc::location!())
|
||||
}
|
||||
|
||||
pub async fn key_delete_prefix(&self, prefix: &[u8]) -> trc::Result<()> {
|
||||
match self {
|
||||
InMemoryStore::Store(store) => {
|
||||
if prefix.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let from_range = prefix.to_vec();
|
||||
let mut to_range = Vec::with_capacity(prefix.len() + 3);
|
||||
to_range.extend_from_slice(prefix);
|
||||
to_range.extend_from_slice([u8::MAX, u8::MAX, u8::MAX].as_ref());
|
||||
|
||||
store
|
||||
.delete_range(
|
||||
ValueKey::from(ValueClass::InMemory(InMemoryClass::Counter(
|
||||
from_range.clone(),
|
||||
))),
|
||||
ValueKey::from(ValueClass::InMemory(InMemoryClass::Counter(
|
||||
to_range.clone(),
|
||||
))),
|
||||
)
|
||||
.await?;
|
||||
|
||||
store
|
||||
.delete_range(
|
||||
ValueKey::from(ValueClass::InMemory(InMemoryClass::Key(from_range))),
|
||||
ValueKey::from(ValueClass::InMemory(InMemoryClass::Key(to_range))),
|
||||
)
|
||||
.await
|
||||
}
|
||||
#[cfg(feature = "redis")]
|
||||
InMemoryStore::Redis(store) => store.key_delete_prefix(prefix).await,
|
||||
InMemoryStore::Static(_) | InMemoryStore::Http(_) => {
|
||||
Err(trc::StoreEvent::NotSupported.into_err())
|
||||
}
|
||||
}
|
||||
.caused_by(trc::location!())
|
||||
}
|
||||
|
||||
pub async fn key_get<T: Deserialize + From<Value<'static>> + std::fmt::Debug + 'static>(
|
||||
&self,
|
||||
key: impl Into<LookupKey<'_>>,
|
||||
) -> trc::Result<Option<T>> {
|
||||
match self {
|
||||
InMemoryStore::Store(store) => store
|
||||
.get_value::<LookupValue<T>>(ValueKey::from(ValueClass::InMemory(
|
||||
InMemoryClass::Key(key.into().into_bytes()),
|
||||
)))
|
||||
.await
|
||||
.map(|value| value.and_then(|v| v.into())),
|
||||
#[cfg(feature = "redis")]
|
||||
InMemoryStore::Redis(store) => store.key_get(key.into().as_bytes()).await,
|
||||
InMemoryStore::Static(store) => Ok(match store.as_ref() {
|
||||
StaticMemoryStore::Map(map) => map
|
||||
.get(key.into().as_str())
|
||||
.map(|value| T::from(value.clone())),
|
||||
StaticMemoryStore::Set(set) => {
|
||||
if set.contains(key.into().as_str()) {
|
||||
Some(T::from(Value::Bool(true)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}),
|
||||
InMemoryStore::Http(store) => {
|
||||
Ok(store.get(key.into().as_str()).map(|value| T::from(value)))
|
||||
}
|
||||
}
|
||||
.caused_by(trc::location!())
|
||||
}
|
||||
|
||||
pub async fn counter_get(&self, key: impl Into<LookupKey<'_>>) -> trc::Result<i64> {
|
||||
match self {
|
||||
InMemoryStore::Store(store) => {
|
||||
store
|
||||
.get_counter(ValueKey::from(ValueClass::InMemory(
|
||||
InMemoryClass::Counter(key.into().into_bytes()),
|
||||
)))
|
||||
.await
|
||||
}
|
||||
#[cfg(feature = "redis")]
|
||||
InMemoryStore::Redis(store) => store.counter_get(key.into().as_bytes()).await,
|
||||
InMemoryStore::Static(_) | InMemoryStore::Http(_) => {
|
||||
Err(trc::StoreEvent::NotSupported.into_err())
|
||||
}
|
||||
}
|
||||
.caused_by(trc::location!())
|
||||
}
|
||||
|
||||
pub async fn key_exists(&self, key: impl Into<LookupKey<'_>>) -> trc::Result<bool> {
|
||||
match self {
|
||||
InMemoryStore::Store(store) => store
|
||||
.get_value::<LookupValue<Empty>>(ValueKey::from(ValueClass::InMemory(
|
||||
InMemoryClass::Key(key.into().into_bytes()),
|
||||
)))
|
||||
.await
|
||||
.map(|value| matches!(value, Some(LookupValue::Value(Empty)))),
|
||||
#[cfg(feature = "redis")]
|
||||
InMemoryStore::Redis(store) => store.key_exists(key.into().as_bytes()).await,
|
||||
InMemoryStore::Static(store) => Ok(match store.as_ref() {
|
||||
StaticMemoryStore::Map(map) => map.get(key.into().as_str()).is_some(),
|
||||
StaticMemoryStore::Set(set) => set.contains(key.into().as_str()),
|
||||
}),
|
||||
InMemoryStore::Http(store) => Ok(store.contains(key.into().as_str())),
|
||||
}
|
||||
.caused_by(trc::location!())
|
||||
}
|
||||
|
||||
pub async fn is_rate_allowed(
|
||||
&self,
|
||||
prefix: u8,
|
||||
key: &[u8],
|
||||
rate: &Rate,
|
||||
soft_check: bool,
|
||||
) -> trc::Result<Option<u64>> {
|
||||
let now = now();
|
||||
let period = rate.period.as_secs().max(1);
|
||||
let range_start = now / period;
|
||||
let range_end = (range_start * period) + period;
|
||||
let expires_in = range_end - now;
|
||||
|
||||
let mut bucket = Vec::with_capacity(key.len() + U64_LEN + 1);
|
||||
bucket.push(prefix);
|
||||
bucket.extend_from_slice(key);
|
||||
bucket.extend_from_slice(range_start.to_be_bytes().as_slice());
|
||||
|
||||
let requests = if !soft_check {
|
||||
self.counter_incr(KeyValue::new(bucket, 1).expires(expires_in), true)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
} else {
|
||||
self.counter_get(bucket).await.caused_by(trc::location!())? + 1
|
||||
};
|
||||
|
||||
if requests <= rate.count as i64 {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(expires_in))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn try_lock(&self, prefix: u8, key: &[u8], duration: u64) -> trc::Result<bool> {
|
||||
match self {
|
||||
InMemoryStore::Store(store) => {
|
||||
let key = KeyValue::<()>::build_key(prefix, key);
|
||||
let lock_expiry = match store
|
||||
.get_value::<u64>(ValueKey::from(ValueClass::InMemory(InMemoryClass::Key(
|
||||
key.clone(),
|
||||
))))
|
||||
.await
|
||||
{
|
||||
Ok(lock_expiry) => lock_expiry,
|
||||
Err(err)
|
||||
if err.matches(trc::EventType::Store(trc::StoreEvent::DataCorruption)) =>
|
||||
{
|
||||
// TODO remove in 1.0
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch.any_op(Operation::Value {
|
||||
class: ValueClass::InMemory(InMemoryClass::Key(key.clone())),
|
||||
op: ValueOp::Clear,
|
||||
});
|
||||
store
|
||||
.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
None
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err
|
||||
.details("Failed to read lock.")
|
||||
.caused_by(trc::location!()));
|
||||
}
|
||||
};
|
||||
|
||||
let now = now();
|
||||
if lock_expiry.is_some_and(|expiry| expiry > now) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let key: ValueClass = ValueClass::InMemory(InMemoryClass::Key(key));
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch.assert_value(
|
||||
key.clone(),
|
||||
match lock_expiry {
|
||||
Some(value) => AssertValue::U64(value),
|
||||
None => AssertValue::None,
|
||||
},
|
||||
);
|
||||
batch.set(key.clone(), (now + duration).serialize());
|
||||
match store.write(batch.build_all()).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(err) if err.is_assertion_failure() => Ok(false),
|
||||
Err(err) => Err(err
|
||||
.details("Failed to lock event.")
|
||||
.caused_by(trc::location!())),
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "redis")]
|
||||
InMemoryStore::Redis(store) => {
|
||||
store
|
||||
.try_lock(&KeyValue::<()>::build_key(prefix, key), duration)
|
||||
.await
|
||||
}
|
||||
InMemoryStore::Static(_) | InMemoryStore::Http(_) => {
|
||||
Err(trc::StoreEvent::NotSupported.into_err())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn remove_lock(&self, prefix: u8, key: &[u8]) -> trc::Result<()> {
|
||||
self.key_delete(KeyValue::<()>::build_key(prefix, key))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn purge_in_memory_store(&self) -> trc::Result<()> {
|
||||
match self {
|
||||
InMemoryStore::Store(store) => {
|
||||
// Delete expired keys and counters
|
||||
let from_key = ValueKey::from(ValueClass::InMemory(InMemoryClass::Key(vec![0u8])));
|
||||
let to_key =
|
||||
ValueKey::from(ValueClass::InMemory(InMemoryClass::Key(vec![u8::MAX; 10])));
|
||||
|
||||
let current_time = now();
|
||||
let mut expired_keys = Vec::new();
|
||||
let mut expired_counters = Vec::new();
|
||||
store
|
||||
.iterate(IterateParams::new(from_key, to_key), |key, value| {
|
||||
let expiry = value.deserialize_be_u64(0).caused_by(trc::location!())?;
|
||||
if expiry == 0 {
|
||||
if value
|
||||
.deserialize_be_u64(U64_LEN)
|
||||
.caused_by(trc::location!())?
|
||||
<= current_time
|
||||
{
|
||||
expired_counters.push(key.to_vec());
|
||||
}
|
||||
} else if expiry <= current_time {
|
||||
expired_keys.push(key.to_vec());
|
||||
}
|
||||
Ok(true)
|
||||
})
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if !expired_keys.is_empty() {
|
||||
let mut batch = BatchBuilder::new();
|
||||
for key in expired_keys {
|
||||
batch.any_op(Operation::Value {
|
||||
class: ValueClass::InMemory(InMemoryClass::Key(key)),
|
||||
op: ValueOp::Clear,
|
||||
});
|
||||
if batch.is_large_batch() {
|
||||
store
|
||||
.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
batch = BatchBuilder::new();
|
||||
}
|
||||
}
|
||||
if !batch.is_empty() {
|
||||
store
|
||||
.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
}
|
||||
|
||||
if !expired_counters.is_empty() {
|
||||
let mut batch = BatchBuilder::new();
|
||||
for key in expired_counters {
|
||||
batch.any_op(Operation::Value {
|
||||
class: ValueClass::InMemory(InMemoryClass::Counter(key.clone())),
|
||||
op: ValueOp::Clear,
|
||||
});
|
||||
batch.any_op(Operation::Value {
|
||||
class: ValueClass::InMemory(InMemoryClass::Key(key)),
|
||||
op: ValueOp::Clear,
|
||||
});
|
||||
if batch.is_large_batch() {
|
||||
store
|
||||
.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
batch = BatchBuilder::new();
|
||||
}
|
||||
}
|
||||
if !batch.is_empty() {
|
||||
store
|
||||
.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "redis")]
|
||||
InMemoryStore::Redis(_) => {}
|
||||
InMemoryStore::Static(_) | InMemoryStore::Http(_) => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_sql(&self) -> bool {
|
||||
match self {
|
||||
InMemoryStore::Store(store) => store.is_sql(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_redis(&self) -> bool {
|
||||
match self {
|
||||
#[cfg(feature = "redis")]
|
||||
InMemoryStore::Redis(_) => true,
|
||||
InMemoryStore::Static(_) => false,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_store(self) -> Option<Store> {
|
||||
match self {
|
||||
InMemoryStore::Store(store) => Some(store),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum LookupKey<'x> {
|
||||
String(String),
|
||||
StringRef(&'x str),
|
||||
Bytes(Vec<u8>),
|
||||
BytesRef(&'x [u8]),
|
||||
}
|
||||
|
||||
impl<'x> From<&'x str> for LookupKey<'x> {
|
||||
fn from(key: &'x str) -> Self {
|
||||
LookupKey::StringRef(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<&'x String> for LookupKey<'x> {
|
||||
fn from(key: &'x String) -> Self {
|
||||
LookupKey::StringRef(key.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<&'x [u8]> for LookupKey<'x> {
|
||||
fn from(key: &'x [u8]) -> Self {
|
||||
LookupKey::BytesRef(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<Cow<'x, str>> for LookupKey<'x> {
|
||||
fn from(key: Cow<'x, str>) -> Self {
|
||||
match key {
|
||||
Cow::Borrowed(key) => LookupKey::StringRef(key),
|
||||
Cow::Owned(key) => LookupKey::String(key),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for LookupKey<'static> {
|
||||
fn from(key: String) -> Self {
|
||||
LookupKey::String(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for LookupKey<'static> {
|
||||
fn from(key: Vec<u8>) -> Self {
|
||||
LookupKey::Bytes(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl LookupKey<'_> {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
LookupKey::String(string) => string,
|
||||
LookupKey::StringRef(string) => string,
|
||||
LookupKey::Bytes(bytes) => std::str::from_utf8(bytes).unwrap_or_default(),
|
||||
LookupKey::BytesRef(bytes) => std::str::from_utf8(bytes).unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
match self {
|
||||
LookupKey::String(string) => string.into_bytes(),
|
||||
LookupKey::StringRef(string) => string.as_bytes().to_vec(),
|
||||
LookupKey::Bytes(bytes) => bytes,
|
||||
LookupKey::BytesRef(bytes) => bytes.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
match self {
|
||||
LookupKey::String(string) => string.as_bytes(),
|
||||
LookupKey::StringRef(string) => string.as_bytes(),
|
||||
LookupKey::Bytes(bytes) => bytes.as_slice(),
|
||||
LookupKey::BytesRef(bytes) => bytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> KeyValue<T> {
|
||||
pub fn build_key(prefix: u8, key: impl AsRef<[u8]>) -> Vec<u8> {
|
||||
let key_ = key.as_ref();
|
||||
let mut key = Vec::with_capacity(key_.len() + 1);
|
||||
key.push(prefix);
|
||||
key.extend_from_slice(key_);
|
||||
key
|
||||
}
|
||||
|
||||
pub fn with_prefix(prefix: u8, key: impl AsRef<[u8]>, value: T) -> Self {
|
||||
Self {
|
||||
key: Self::build_key(prefix, key),
|
||||
value,
|
||||
expires: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(key: impl Into<Vec<u8>>, value: T) -> Self {
|
||||
Self {
|
||||
key: key.into(),
|
||||
value,
|
||||
expires: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expires(mut self, expires: u64) -> Self {
|
||||
self.expires = expires.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn expires_opt(mut self, expires: Option<u64>) -> Self {
|
||||
self.expires = expires;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct Empty;
|
||||
|
||||
enum LookupValue<T> {
|
||||
Value(T),
|
||||
None,
|
||||
}
|
||||
|
||||
impl<T: Deserialize> Deserialize for LookupValue<T> {
|
||||
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
|
||||
bytes.deserialize_be_u64(0).and_then(|expires| {
|
||||
Ok(if expires > now() {
|
||||
LookupValue::Value(
|
||||
T::deserialize(bytes.get(U64_LEN..).unwrap_or_default())
|
||||
.caused_by(trc::location!())?,
|
||||
)
|
||||
} else {
|
||||
LookupValue::None
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for Empty {
|
||||
fn deserialize(_bytes: &[u8]) -> trc::Result<Self> {
|
||||
Ok(Empty)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<LookupValue<T>> for Option<T> {
|
||||
fn from(value: LookupValue<T>) -> Self {
|
||||
match value {
|
||||
LookupValue::Value(value) => Some(value),
|
||||
LookupValue::None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Value<'static>> for String {
|
||||
fn from(value: Value<'static>) -> Self {
|
||||
match value {
|
||||
Value::Text(string) => string.into_owned(),
|
||||
Value::Blob(bytes) => String::from_utf8_lossy(bytes.as_ref()).into_owned(),
|
||||
Value::Bool(boolean) => boolean.to_string(),
|
||||
Value::Null => String::new(),
|
||||
Value::Integer(num) => num.to_string(),
|
||||
Value::Float(num) => num.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::Store;
|
||||
use roaring::RoaringBitmap;
|
||||
|
||||
pub mod blob;
|
||||
pub mod lookup;
|
||||
pub mod search;
|
||||
pub mod store;
|
||||
|
||||
impl Store {
|
||||
pub fn id(&self) -> &'static str {
|
||||
match self {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Self::SQLite(_) => "sqlite",
|
||||
#[cfg(feature = "foundation")]
|
||||
Self::FoundationDb(_) => "foundationdb",
|
||||
#[cfg(feature = "postgres")]
|
||||
Self::PostgreSQL(_) => "postgresql",
|
||||
#[cfg(feature = "mysql")]
|
||||
Self::MySQL(_) => "mysql",
|
||||
#[cfg(feature = "rocks")]
|
||||
Self::RocksDb(_) => "rocksdb",
|
||||
Self::Ephemeral(_) => "ephemeral",
|
||||
Self::None => "none",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub trait DocumentSet: Sync + Send {
|
||||
fn min(&self) -> u32;
|
||||
fn max(&self) -> u32;
|
||||
fn contains(&self, id: u32) -> bool;
|
||||
fn len(&self) -> usize;
|
||||
fn iterate(&self) -> impl Iterator<Item = u32>;
|
||||
}
|
||||
|
||||
impl DocumentSet for RoaringBitmap {
|
||||
fn min(&self) -> u32 {
|
||||
self.min().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn max(&self) -> u32 {
|
||||
self.max().map(|m| m + 1).unwrap_or(0)
|
||||
}
|
||||
|
||||
fn contains(&self, id: u32) -> bool {
|
||||
self.contains(id)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.len() as usize
|
||||
}
|
||||
|
||||
fn iterate(&self) -> impl Iterator<Item = u32> {
|
||||
self.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl DocumentSet for Vec<u32> {
|
||||
fn contains(&self, id: u32) -> bool {
|
||||
self.binary_search(&id).is_ok()
|
||||
}
|
||||
|
||||
fn min(&self) -> u32 {
|
||||
self.first().copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn max(&self) -> u32 {
|
||||
self.last().copied().map(|m| m + 1).unwrap_or(0)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn iterate(&self) -> impl Iterator<Item = u32> {
|
||||
self.iter().copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl DocumentSet for () {
|
||||
fn min(&self) -> u32 {
|
||||
0
|
||||
}
|
||||
|
||||
fn max(&self) -> u32 {
|
||||
u32::MAX
|
||||
}
|
||||
|
||||
fn contains(&self, _: u32) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
fn iterate(&self) -> impl Iterator<Item = u32> {
|
||||
std::iter::empty()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
SearchStore, Store,
|
||||
search::{
|
||||
IndexDocument, SearchComparator, SearchField, SearchFilter, SearchOperator, SearchQuery,
|
||||
SearchValue,
|
||||
split::{SplitFilter, split_filters},
|
||||
},
|
||||
write::SearchIndex,
|
||||
};
|
||||
use std::cmp::Ordering;
|
||||
use trc::AddContext;
|
||||
|
||||
impl SearchStore {
|
||||
pub async fn query_account(&self, query: SearchQuery) -> trc::Result<Vec<u32>> {
|
||||
// Pre-filter by mask
|
||||
if query.mask.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
// If the store does not support FTS, use the internal FTS store
|
||||
if let Some(store) = self.internal_fts() {
|
||||
return store.query_account(query).await;
|
||||
}
|
||||
|
||||
// If all filters and comparators are external, delegate to the underlying store
|
||||
let mut account_id = u32::MAX;
|
||||
let mut has_local_filters = false;
|
||||
let mut has_external_filters = false;
|
||||
for filter in &query.filters {
|
||||
match filter {
|
||||
SearchFilter::Operator {
|
||||
field: SearchField::AccountId,
|
||||
op: SearchOperator::Equal,
|
||||
value: SearchValue::Uint(id),
|
||||
} => {
|
||||
account_id = *id as u32;
|
||||
}
|
||||
SearchFilter::DocumentSet(_) => {
|
||||
has_local_filters = true;
|
||||
}
|
||||
SearchFilter::Operator { .. } => {
|
||||
has_external_filters = true;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
if account_id == u32::MAX {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.reason("Account ID filter is required for account queries")
|
||||
.caused_by(trc::location!()));
|
||||
}
|
||||
|
||||
if !has_local_filters && !has_external_filters && query.comparators.is_empty() {
|
||||
return Ok(query.mask.iter().collect());
|
||||
}
|
||||
|
||||
if !has_local_filters && query.comparators.iter().all(|c| c.is_external()) {
|
||||
return self
|
||||
.sub_query(query.index, &query.filters, &query.comparators)
|
||||
.await
|
||||
.map(|results| {
|
||||
if !results.is_empty() || has_external_filters {
|
||||
results
|
||||
.into_iter()
|
||||
.filter(|id| query.mask.contains(*id))
|
||||
.collect()
|
||||
} else {
|
||||
// Database sort is broken, return masked results
|
||||
query.mask.iter().collect()
|
||||
}
|
||||
})
|
||||
.caused_by(trc::location!());
|
||||
}
|
||||
|
||||
let filters = if has_external_filters {
|
||||
// Split filters
|
||||
let split_filters = split_filters(query.filters).ok_or_else(|| {
|
||||
trc::StoreEvent::UnexpectedError
|
||||
.reason("Invalid filter query")
|
||||
.caused_by(trc::location!())
|
||||
})?;
|
||||
|
||||
let mut filters = Vec::with_capacity(split_filters.len());
|
||||
for split_filter in split_filters {
|
||||
match split_filter {
|
||||
SplitFilter::External(external) => {
|
||||
// Execute sub-query
|
||||
filters.push(SearchFilter::DocumentSet(
|
||||
self.sub_query(query.index, &external, &[])
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect(),
|
||||
));
|
||||
}
|
||||
SplitFilter::Internal(filter) => {
|
||||
filters.push(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filters
|
||||
} else {
|
||||
query.filters
|
||||
};
|
||||
|
||||
// Merge results locally
|
||||
let results = SearchQuery::new(query.index)
|
||||
.with_filters(filters)
|
||||
.with_mask(query.mask)
|
||||
.filter();
|
||||
|
||||
let total_results = results.results().len();
|
||||
match total_results.cmp(&1) {
|
||||
Ordering::Equal => Ok(vec![results.results().min().unwrap()]),
|
||||
Ordering::Less => Ok(vec![]),
|
||||
Ordering::Greater => {
|
||||
if !query.comparators.is_empty() {
|
||||
let mut local = Vec::with_capacity(query.comparators.len());
|
||||
let mut external = Vec::with_capacity(query.comparators.len());
|
||||
let mut external_first = false;
|
||||
for (pos, comparator) in query.comparators.into_iter().enumerate() {
|
||||
if comparator.is_external() {
|
||||
external.push(comparator);
|
||||
if pos == 0 {
|
||||
external_first = true;
|
||||
}
|
||||
} else {
|
||||
local.push(comparator);
|
||||
}
|
||||
}
|
||||
|
||||
if !external.is_empty() {
|
||||
let mut results = results.results().clone();
|
||||
let filters = vec![
|
||||
SearchFilter::Operator {
|
||||
field: SearchField::AccountId,
|
||||
op: SearchOperator::Equal,
|
||||
value: SearchValue::Uint(account_id as u64),
|
||||
},
|
||||
SearchFilter::Operator {
|
||||
field: SearchField::DocumentId,
|
||||
op: SearchOperator::GreaterEqualThan,
|
||||
value: SearchValue::Uint(results.min().unwrap() as u64),
|
||||
},
|
||||
SearchFilter::Operator {
|
||||
field: SearchField::DocumentId,
|
||||
op: SearchOperator::LowerEqualThan,
|
||||
value: SearchValue::Uint(results.max().unwrap() as u64),
|
||||
},
|
||||
];
|
||||
|
||||
let mut ordered_results = Vec::with_capacity(total_results as usize);
|
||||
for ordered_result in
|
||||
self.sub_query(query.index, &filters, &external).await?
|
||||
{
|
||||
if results.remove(ordered_result) {
|
||||
ordered_results.push(ordered_result);
|
||||
}
|
||||
}
|
||||
// Add any remaining results not yet in the index
|
||||
ordered_results.extend(results);
|
||||
|
||||
if local.is_empty() {
|
||||
return Ok(ordered_results);
|
||||
}
|
||||
|
||||
let comparator = SearchComparator::SortedSet {
|
||||
set: ordered_results
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(pos, id)| (id, pos as u32))
|
||||
.collect(),
|
||||
ascending: true,
|
||||
};
|
||||
|
||||
if external_first {
|
||||
local.insert(0, comparator);
|
||||
} else {
|
||||
local.push(comparator);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results.with_comparators(local).into_sorted())
|
||||
} else {
|
||||
Ok(results.results().iter().collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn sub_query(
|
||||
&self,
|
||||
index: SearchIndex,
|
||||
filters: &[SearchFilter],
|
||||
sort: &[SearchComparator],
|
||||
) -> trc::Result<Vec<u32>> {
|
||||
match self {
|
||||
SearchStore::Store(store) => match store {
|
||||
#[cfg(feature = "postgres")]
|
||||
Store::PostgreSQL(store) => store.query(index, filters, sort).await,
|
||||
#[cfg(feature = "mysql")]
|
||||
Store::MySQL(store) => store.query(index, filters, sort).await,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
SearchStore::ElasticSearch(store) => store.query(index, filters, sort).await,
|
||||
SearchStore::MeiliSearch(store) => store.query(index, filters, sort).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn query_global(&self, query: SearchQuery) -> trc::Result<Vec<u64>> {
|
||||
match self {
|
||||
SearchStore::Store(store) => match store {
|
||||
#[cfg(feature = "postgres")]
|
||||
Store::PostgreSQL(store) => {
|
||||
store
|
||||
.query(query.index, &query.filters, &query.comparators)
|
||||
.await
|
||||
}
|
||||
#[cfg(feature = "mysql")]
|
||||
Store::MySQL(store) => {
|
||||
store
|
||||
.query(query.index, &query.filters, &query.comparators)
|
||||
.await
|
||||
}
|
||||
store => store.query_global(query).await,
|
||||
},
|
||||
SearchStore::ElasticSearch(store) => {
|
||||
store
|
||||
.query(query.index, &query.filters, &query.comparators)
|
||||
.await
|
||||
}
|
||||
SearchStore::MeiliSearch(store) => {
|
||||
store
|
||||
.query(query.index, &query.filters, &query.comparators)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn index(&self, documents: Vec<IndexDocument>) -> trc::Result<()> {
|
||||
match self {
|
||||
SearchStore::Store(store) => match store {
|
||||
#[cfg(feature = "postgres")]
|
||||
Store::PostgreSQL(store) => store.index(documents).await,
|
||||
#[cfg(feature = "mysql")]
|
||||
Store::MySQL(store) => store.index(documents).await,
|
||||
store => store.index(documents).await,
|
||||
},
|
||||
SearchStore::ElasticSearch(store) => store.index(documents).await,
|
||||
SearchStore::MeiliSearch(store) => store.index(documents).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn unindex(&self, query: SearchQuery) -> trc::Result<u64> {
|
||||
match self {
|
||||
SearchStore::Store(store) => match store {
|
||||
#[cfg(feature = "postgres")]
|
||||
Store::PostgreSQL(store) => store.unindex(query).await,
|
||||
#[cfg(feature = "mysql")]
|
||||
Store::MySQL(store) => store.unindex(query).await,
|
||||
store => store.unindex(query).await.map(|_| 0),
|
||||
},
|
||||
SearchStore::ElasticSearch(store) => store.unindex(query).await,
|
||||
SearchStore::MeiliSearch(store) => store.unindex(query).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn internal_fts(&self) -> Option<&Store> {
|
||||
match self {
|
||||
SearchStore::Store(store) => match store {
|
||||
#[cfg(feature = "postgres")]
|
||||
Store::PostgreSQL(_) => None,
|
||||
#[cfg(feature = "mysql")]
|
||||
Store::MySQL(_) => None,
|
||||
store => Some(store),
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_mysql(&self) -> bool {
|
||||
match self {
|
||||
#[cfg(feature = "mysql")]
|
||||
SearchStore::Store(Store::MySQL(_)) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_postgres(&self) -> bool {
|
||||
match self {
|
||||
#[cfg(feature = "postgres")]
|
||||
SearchStore::Store(Store::PostgreSQL(_)) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_elasticsearch(&self) -> bool {
|
||||
matches!(self, SearchStore::ElasticSearch(_))
|
||||
}
|
||||
|
||||
pub fn is_meilisearch(&self) -> bool {
|
||||
matches!(self, SearchStore::MeiliSearch(_))
|
||||
}
|
||||
|
||||
pub async fn create_indexes(&self) -> trc::Result<()> {
|
||||
match self {
|
||||
SearchStore::Store(store) => match store {
|
||||
#[cfg(feature = "postgres")]
|
||||
Store::PostgreSQL(store) => store.create_search_tables().await,
|
||||
#[cfg(feature = "mysql")]
|
||||
Store::MySQL(store) => store.create_search_tables().await,
|
||||
_ => Ok(()),
|
||||
},
|
||||
SearchStore::ElasticSearch(store) => store.create_indexes().await,
|
||||
SearchStore::MeiliSearch(store) => store.create_indexes().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchFilter {
|
||||
pub fn is_external(&self) -> bool {
|
||||
matches!(self, SearchFilter::Operator { .. })
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchComparator {
|
||||
pub fn is_external(&self) -> bool {
|
||||
matches!(self, SearchComparator::Field { .. })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::DocumentSet;
|
||||
use crate::{
|
||||
Deserialize, IterateParams, Key, QueryResult, SUBSPACE_COUNTER, SUBSPACE_INDEXES,
|
||||
SUBSPACE_LOGS, Store, U32_LEN, Value, ValueKey,
|
||||
write::{
|
||||
AnyClass, AnyKey, AssignedIds, Batch, BatchBuilder, Operation, ValueClass, ValueOp,
|
||||
key::{DeserializeBigEndian, KeySerializer},
|
||||
},
|
||||
};
|
||||
use compact_str::ToCompactString;
|
||||
use std::time::Instant;
|
||||
use trc::{AddContext, StoreEvent};
|
||||
use types::collection::Collection;
|
||||
|
||||
impl Store {
|
||||
pub async fn get_value<U>(&self, key: impl Key) -> trc::Result<Option<U>>
|
||||
where
|
||||
U: Deserialize + 'static,
|
||||
{
|
||||
match self {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Self::SQLite(store) => store.get_value(key).await,
|
||||
#[cfg(feature = "foundation")]
|
||||
Self::FoundationDb(store) => store.get_value(key).await,
|
||||
#[cfg(feature = "postgres")]
|
||||
Self::PostgreSQL(store) => store.get_value(key).await,
|
||||
#[cfg(feature = "mysql")]
|
||||
Self::MySQL(store) => store.get_value(key).await,
|
||||
#[cfg(feature = "rocks")]
|
||||
Self::RocksDb(store) => store.get_value(key).await,
|
||||
Self::Ephemeral(store) => store.get_value(key).await,
|
||||
Self::None => Err(trc::StoreEvent::NotConfigured.into()),
|
||||
}
|
||||
.caused_by(trc::location!())
|
||||
}
|
||||
|
||||
pub async fn key_exists(&self, key: impl Key) -> trc::Result<bool> {
|
||||
match self {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Self::SQLite(store) => store.key_exists(key).await,
|
||||
#[cfg(feature = "foundation")]
|
||||
Self::FoundationDb(store) => store.key_exists(key).await,
|
||||
#[cfg(feature = "postgres")]
|
||||
Self::PostgreSQL(store) => store.key_exists(key).await,
|
||||
#[cfg(feature = "mysql")]
|
||||
Self::MySQL(store) => store.key_exists(key).await,
|
||||
#[cfg(feature = "rocks")]
|
||||
Self::RocksDb(store) => store.key_exists(key).await,
|
||||
Self::Ephemeral(store) => store.key_exists(key).await,
|
||||
Self::None => Err(trc::StoreEvent::NotConfigured.into()),
|
||||
}
|
||||
.caused_by(trc::location!())
|
||||
}
|
||||
|
||||
pub async fn iterate<T: Key>(
|
||||
&self,
|
||||
params: IterateParams<T>,
|
||||
cb: impl for<'x> FnMut(&'x [u8], &'x [u8]) -> trc::Result<bool> + Sync + Send,
|
||||
) -> trc::Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let result = match self {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Self::SQLite(store) => store.iterate(params, cb).await,
|
||||
#[cfg(feature = "foundation")]
|
||||
Self::FoundationDb(store) => store.iterate(params, cb).await,
|
||||
#[cfg(feature = "postgres")]
|
||||
Self::PostgreSQL(store) => store.iterate(params, cb).await,
|
||||
#[cfg(feature = "mysql")]
|
||||
Self::MySQL(store) => store.iterate(params, cb).await,
|
||||
#[cfg(feature = "rocks")]
|
||||
Self::RocksDb(store) => store.iterate(params, cb).await,
|
||||
Self::Ephemeral(store) => store.iterate(params, cb).await,
|
||||
Self::None => Err(trc::StoreEvent::NotConfigured.into()),
|
||||
}
|
||||
.caused_by(trc::location!());
|
||||
|
||||
trc::event!(
|
||||
Store(StoreEvent::DataIterate),
|
||||
Elapsed = start_time.elapsed(),
|
||||
);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub async fn get_counter(
|
||||
&self,
|
||||
key: impl Into<ValueKey<ValueClass>> + Sync + Send,
|
||||
) -> trc::Result<i64> {
|
||||
match self {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Self::SQLite(store) => store.get_counter(key).await,
|
||||
#[cfg(feature = "foundation")]
|
||||
Self::FoundationDb(store) => store.get_counter(key).await,
|
||||
#[cfg(feature = "postgres")]
|
||||
Self::PostgreSQL(store) => store.get_counter(key).await,
|
||||
#[cfg(feature = "mysql")]
|
||||
Self::MySQL(store) => store.get_counter(key).await,
|
||||
#[cfg(feature = "rocks")]
|
||||
Self::RocksDb(store) => store.get_counter(key).await,
|
||||
Self::Ephemeral(store) => store.get_counter(key).await,
|
||||
Self::None => Err(trc::StoreEvent::NotConfigured.into()),
|
||||
}
|
||||
.caused_by(trc::location!())
|
||||
}
|
||||
|
||||
#[allow(unreachable_patterns)]
|
||||
#[allow(unused_variables)]
|
||||
pub async fn sql_query<T: QueryResult + std::fmt::Debug>(
|
||||
&self,
|
||||
query: &str,
|
||||
params: Vec<Value<'_>>,
|
||||
) -> trc::Result<T> {
|
||||
let result = match self {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Self::SQLite(store) => store.sql_query(query, ¶ms).await,
|
||||
#[cfg(feature = "postgres")]
|
||||
Self::PostgreSQL(store) => store.sql_query(query, ¶ms).await,
|
||||
#[cfg(feature = "mysql")]
|
||||
Self::MySQL(store) => store.sql_query(query, ¶ms).await,
|
||||
_ => Err(trc::StoreEvent::NotSupported.into_err()),
|
||||
};
|
||||
|
||||
trc::event!(
|
||||
Store(trc::StoreEvent::SqlQuery),
|
||||
Details = query.to_compact_string(),
|
||||
Value = params.as_slice(),
|
||||
Result = &result,
|
||||
);
|
||||
|
||||
result.caused_by(trc::location!())
|
||||
}
|
||||
|
||||
pub async fn write(&self, batch: Batch<'_>) -> trc::Result<AssignedIds> {
|
||||
let start_time = Instant::now();
|
||||
let ops = batch.ops.len();
|
||||
|
||||
let result = match self {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Self::SQLite(store) => store.write(batch).await,
|
||||
#[cfg(feature = "foundation")]
|
||||
Self::FoundationDb(store) => store.write(batch).await,
|
||||
#[cfg(feature = "postgres")]
|
||||
Self::PostgreSQL(store) => store.write(batch).await,
|
||||
#[cfg(feature = "mysql")]
|
||||
Self::MySQL(store) => store.write(batch).await,
|
||||
#[cfg(feature = "rocks")]
|
||||
Self::RocksDb(store) => store.write(batch).await,
|
||||
Self::Ephemeral(store) => store.write(batch).await,
|
||||
Self::None => Err(trc::StoreEvent::NotConfigured.into()),
|
||||
};
|
||||
|
||||
trc::event!(
|
||||
Store(StoreEvent::DataWrite),
|
||||
Elapsed = start_time.elapsed(),
|
||||
Total = ops,
|
||||
);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub async fn assign_document_ids(
|
||||
&self,
|
||||
account_id: u32,
|
||||
collection: Collection,
|
||||
num_ids: u64,
|
||||
) -> trc::Result<u32> {
|
||||
// Increment UID next
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(collection)
|
||||
.add_and_get(ValueClass::DocumentId, num_ids as i64);
|
||||
self.write(batch.build_all()).await.and_then(|v| {
|
||||
v.last_counter_id().map(|id| {
|
||||
debug_assert!(id >= num_ids as i64, "{} < {}", id, num_ids);
|
||||
id as u32
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn purge_store(&self) -> trc::Result<()> {
|
||||
match self {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Self::SQLite(store) => store.purge_store().await,
|
||||
#[cfg(feature = "foundation")]
|
||||
Self::FoundationDb(store) => store.purge_store().await,
|
||||
#[cfg(feature = "postgres")]
|
||||
Self::PostgreSQL(store) => store.purge_store().await,
|
||||
#[cfg(feature = "mysql")]
|
||||
Self::MySQL(store) => store.purge_store().await,
|
||||
#[cfg(feature = "rocks")]
|
||||
Self::RocksDb(store) => store.purge_store().await,
|
||||
Self::Ephemeral(store) => store.purge_store().await,
|
||||
Self::None => Err(trc::StoreEvent::NotConfigured.into()),
|
||||
}
|
||||
.caused_by(trc::location!())
|
||||
}
|
||||
|
||||
pub async fn delete_range(&self, from: impl Key, to: impl Key) -> trc::Result<()> {
|
||||
match self {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Self::SQLite(store) => store.delete_range(from, to).await,
|
||||
#[cfg(feature = "foundation")]
|
||||
Self::FoundationDb(store) => store.delete_range(from, to).await,
|
||||
#[cfg(feature = "postgres")]
|
||||
Self::PostgreSQL(store) => store.delete_range(from, to).await,
|
||||
#[cfg(feature = "mysql")]
|
||||
Self::MySQL(store) => store.delete_range(from, to).await,
|
||||
#[cfg(feature = "rocks")]
|
||||
Self::RocksDb(store) => store.delete_range(from, to).await,
|
||||
Self::Ephemeral(store) => store.delete_range(from, to).await,
|
||||
Self::None => Err(trc::StoreEvent::NotConfigured.into()),
|
||||
}
|
||||
.caused_by(trc::location!())
|
||||
}
|
||||
|
||||
pub async fn delete_documents(
|
||||
&self,
|
||||
subspace: u8,
|
||||
account_id: u32,
|
||||
collection: u8,
|
||||
collection_offset: Option<usize>,
|
||||
document_ids: &impl DocumentSet,
|
||||
) -> trc::Result<()> {
|
||||
// Serialize keys
|
||||
let (from_key, to_key) = if collection_offset.is_some() {
|
||||
(
|
||||
KeySerializer::new(U32_LEN + 2)
|
||||
.write(account_id)
|
||||
.write(collection),
|
||||
KeySerializer::new(U32_LEN + 2)
|
||||
.write(account_id)
|
||||
.write(collection + 1),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
KeySerializer::new(U32_LEN).write(account_id),
|
||||
KeySerializer::new(U32_LEN).write(account_id + 1),
|
||||
)
|
||||
};
|
||||
|
||||
// Find keys to delete
|
||||
let mut delete_keys = Vec::new();
|
||||
self.iterate(
|
||||
IterateParams::new(
|
||||
AnyKey {
|
||||
subspace,
|
||||
key: from_key.finalize(),
|
||||
},
|
||||
AnyKey {
|
||||
subspace,
|
||||
key: to_key.finalize(),
|
||||
},
|
||||
)
|
||||
.no_values(),
|
||||
|key, _| {
|
||||
if collection_offset.is_none_or(|offset| {
|
||||
key.get(key.len() - U32_LEN - offset).copied() == Some(collection)
|
||||
}) {
|
||||
let document_id = key.deserialize_be_u32(key.len() - U32_LEN)?;
|
||||
if document_ids.contains(document_id) {
|
||||
delete_keys.push(key.to_vec());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Remove keys
|
||||
let mut batch = BatchBuilder::new();
|
||||
|
||||
for key in delete_keys {
|
||||
if batch.is_large_batch() {
|
||||
self.write(std::mem::take(&mut batch).build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
batch.any_op(Operation::Value {
|
||||
class: ValueClass::Any(AnyClass { subspace, key }),
|
||||
op: ValueOp::Clear,
|
||||
});
|
||||
}
|
||||
|
||||
if !batch.is_empty() {
|
||||
self.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn danger_destroy_account(&self, account_id: u32) -> trc::Result<()> {
|
||||
for subspace in [SUBSPACE_LOGS, SUBSPACE_INDEXES, SUBSPACE_COUNTER] {
|
||||
self.delete_range(
|
||||
AnyKey {
|
||||
subspace,
|
||||
key: KeySerializer::new(U32_LEN).write(account_id).finalize(),
|
||||
},
|
||||
AnyKey {
|
||||
subspace,
|
||||
key: KeySerializer::new(U32_LEN).write(account_id + 1).finalize(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
self.delete_range(
|
||||
ValueKey {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
class: ValueClass::Acl(account_id),
|
||||
},
|
||||
ValueKey {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
class: ValueClass::Acl(account_id + 1),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
self.delete_range(
|
||||
ValueKey {
|
||||
account_id,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
class: ValueClass::Property(0),
|
||||
},
|
||||
ValueKey {
|
||||
account_id: account_id + 1,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
class: ValueClass::Property(0),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_tables(&self) -> trc::Result<()> {
|
||||
match self {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Self::SQLite(store) => store.create_tables(),
|
||||
#[cfg(feature = "postgres")]
|
||||
Self::PostgreSQL(store) => store.create_storage_tables().await,
|
||||
#[cfg(feature = "mysql")]
|
||||
Self::MySQL(store) => store.create_storage_tables().await,
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalidate_read_snapshot(&self) {
|
||||
#[cfg(feature = "foundation")]
|
||||
if let Self::FoundationDb(store) = self {
|
||||
store.invalidate_read_snapshot();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,737 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
#![warn(clippy::large_futures)]
|
||||
|
||||
pub mod backend;
|
||||
pub mod build;
|
||||
pub mod dispatch;
|
||||
pub mod query;
|
||||
pub mod registry;
|
||||
pub mod search;
|
||||
pub mod write;
|
||||
|
||||
use ::registry::schema::enums::CompressionAlgo;
|
||||
pub use ahash;
|
||||
pub use blake3;
|
||||
pub use parking_lot;
|
||||
pub use rand;
|
||||
pub use rkyv;
|
||||
pub use roaring;
|
||||
use utils::snowflake::SnowflakeIdGenerator;
|
||||
pub use xxhash_rust;
|
||||
|
||||
use crate::backend::{elastic::ElasticSearchStore, meili::MeiliSearchStore};
|
||||
use ahash::AHashMap;
|
||||
use backend::{ephemeral::EphemeralStore, fs::FsStore, http::HttpStore, memory::StaticMemoryStore};
|
||||
use std::{borrow::Cow, path::PathBuf, sync::Arc};
|
||||
use write::ValueClass;
|
||||
|
||||
pub trait Deserialize: Sized + Sync + Send {
|
||||
fn deserialize(bytes: &[u8]) -> trc::Result<Self>;
|
||||
|
||||
#[inline(always)]
|
||||
fn deserialize_owned(bytes: Vec<u8>) -> trc::Result<Self> {
|
||||
Self::deserialize(&bytes)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn deserialize_with_key(_: &[u8], bytes: &[u8]) -> trc::Result<Self> {
|
||||
Self::deserialize(bytes)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn deserialize_owned_with_key(key: &[u8], bytes: Vec<u8>) -> trc::Result<Self> {
|
||||
Self::deserialize_with_key(key, &bytes)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Serialize {
|
||||
fn serialize(&self) -> trc::Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
pub trait SerializeInfallible {
|
||||
fn serialize(&self) -> Vec<u8>;
|
||||
}
|
||||
|
||||
// Key serialization flags
|
||||
pub(crate) const WITH_SUBSPACE: u32 = 1;
|
||||
|
||||
pub trait Key: Sync + Send + Clone {
|
||||
fn serialize(&self, flags: u32) -> Vec<u8>;
|
||||
fn subspace(&self) -> u8;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct IndexKey<T: AsRef<[u8]>> {
|
||||
pub account_id: u32,
|
||||
pub collection: u8,
|
||||
pub document_id: u32,
|
||||
pub field: u8,
|
||||
pub key: T,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct IndexKeyPrefix {
|
||||
pub account_id: u32,
|
||||
pub collection: u8,
|
||||
pub field: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ValueKey<T: AsRef<ValueClass>> {
|
||||
pub account_id: u32,
|
||||
pub collection: u8,
|
||||
pub document_id: u32,
|
||||
pub class: T,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct LogKey {
|
||||
pub account_id: u32,
|
||||
pub collection: u8,
|
||||
pub change_id: u64,
|
||||
}
|
||||
|
||||
pub const U64_LEN: usize = std::mem::size_of::<u64>();
|
||||
pub const U32_LEN: usize = std::mem::size_of::<u32>();
|
||||
pub const U16_LEN: usize = std::mem::size_of::<u16>();
|
||||
|
||||
pub const SUBSPACE_ACL: u8 = b'a';
|
||||
pub const SUBSPACE_TASK_QUEUE: u8 = b'f';
|
||||
pub const SUBSPACE_INDEXES: u8 = b'i';
|
||||
pub const SUBSPACE_BLOB_LINK: u8 = b'k';
|
||||
pub const SUBSPACE_BLOBS: u8 = b't';
|
||||
pub const SUBSPACE_LOGS: u8 = b'l';
|
||||
pub const SUBSPACE_COUNTER: u8 = b'n';
|
||||
pub const SUBSPACE_IN_MEMORY_VALUE: u8 = b'm';
|
||||
pub const SUBSPACE_IN_MEMORY_COUNTER: u8 = b'y';
|
||||
pub const SUBSPACE_PROPERTY: u8 = b'p';
|
||||
pub const SUBSPACE_REGISTRY: u8 = b's';
|
||||
pub const SUBSPACE_REGISTRY_IDX: u8 = b'b';
|
||||
pub const SUBSPACE_REGISTRY_PK: u8 = b'g';
|
||||
pub const SUBSPACE_DIRECTORY: u8 = b'd';
|
||||
pub const SUBSPACE_QUEUE_MESSAGE: u8 = b'e';
|
||||
pub const SUBSPACE_QUEUE_EVENT: u8 = b'q';
|
||||
pub const SUBSPACE_QUOTA: u8 = b'u';
|
||||
pub const SUBSPACE_REPORT_OUT: u8 = b'h';
|
||||
pub const SUBSPACE_REPORT_IN: u8 = b'r';
|
||||
pub const SUBSPACE_TELEMETRY_SPAN: u8 = b'o';
|
||||
pub const SUBSPACE_TELEMETRY_METRIC: u8 = b'x';
|
||||
pub const SUBSPACE_SEARCH_INDEX: u8 = b'z';
|
||||
pub const SUBSPACE_DELETED_ITEMS: u8 = b'j';
|
||||
pub const SUBSPACE_SPAM_SAMPLES: u8 = b'w';
|
||||
|
||||
// TODO: Remove in v1.0
|
||||
pub const LEGACY_SUBSPACE_BITMAP_TEXT: u8 = b'v';
|
||||
pub const LEGACY_SUBSPACE_BITMAP_TAG: u8 = b'c';
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IterateParams<T: Key> {
|
||||
begin: T,
|
||||
end: T,
|
||||
first: bool,
|
||||
ascending: bool,
|
||||
values: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct LookupStores {
|
||||
pub stores: AHashMap<Box<str>, InMemoryStore>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub enum Store {
|
||||
#[cfg(feature = "sqlite")]
|
||||
SQLite(Arc<backend::sqlite::SqliteStore>),
|
||||
#[cfg(feature = "foundation")]
|
||||
FoundationDb(Arc<backend::foundationdb::FdbStore>),
|
||||
#[cfg(feature = "postgres")]
|
||||
PostgreSQL(Arc<backend::postgres::PostgresStore>),
|
||||
#[cfg(feature = "mysql")]
|
||||
MySQL(Arc<backend::mysql::MysqlStore>),
|
||||
#[cfg(feature = "rocks")]
|
||||
RocksDb(Arc<backend::rocksdb::RocksDbStore>),
|
||||
Ephemeral(Arc<EphemeralStore>),
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum BlobStore {
|
||||
Store(Store),
|
||||
Fs(Arc<FsStore>),
|
||||
#[cfg(feature = "s3")]
|
||||
S3(Arc<backend::s3::S3Store>),
|
||||
#[cfg(feature = "azure")]
|
||||
Azure(Arc<backend::azure::AzureStore>),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum SearchStore {
|
||||
Store(Store),
|
||||
ElasticSearch(Arc<ElasticSearchStore>),
|
||||
MeiliSearch(Arc<MeiliSearchStore>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum InMemoryStore {
|
||||
Store(Store),
|
||||
#[cfg(feature = "redis")]
|
||||
Redis(Arc<backend::redis::RedisStore>),
|
||||
Http(Arc<HttpStore>),
|
||||
Static(Arc<StaticMemoryStore>),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RegistryStore(pub(crate) Arc<RegistryStoreInner>);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RegistryStoreInner {
|
||||
pub(crate) local_path: PathBuf,
|
||||
pub(crate) store: Store,
|
||||
pub(crate) node_id: u16,
|
||||
pub(crate) env_recovery_mode: bool,
|
||||
pub(crate) env_recovery_admin: Option<(String, String)>,
|
||||
pub(crate) env_cluster_role: Option<String>,
|
||||
pub(crate) env_push_shard_id: u32,
|
||||
pub(crate) env_hostname: String,
|
||||
pub(crate) env_public_url: Option<String>,
|
||||
pub(crate) id_generator: SnowflakeIdGenerator,
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
impl From<backend::sqlite::SqliteStore> for Store {
|
||||
fn from(store: backend::sqlite::SqliteStore) -> Self {
|
||||
Self::SQLite(Arc::new(store))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "foundation")]
|
||||
impl From<backend::foundationdb::FdbStore> for Store {
|
||||
fn from(store: backend::foundationdb::FdbStore) -> Self {
|
||||
Self::FoundationDb(Arc::new(store))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
impl From<backend::postgres::PostgresStore> for Store {
|
||||
fn from(store: backend::postgres::PostgresStore) -> Self {
|
||||
Self::PostgreSQL(Arc::new(store))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "mysql")]
|
||||
impl From<backend::mysql::MysqlStore> for Store {
|
||||
fn from(store: backend::mysql::MysqlStore) -> Self {
|
||||
Self::MySQL(Arc::new(store))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rocks")]
|
||||
impl From<backend::rocksdb::RocksDbStore> for Store {
|
||||
fn from(store: backend::rocksdb::RocksDbStore) -> Self {
|
||||
Self::RocksDb(Arc::new(store))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EphemeralStore> for Store {
|
||||
fn from(store: EphemeralStore) -> Self {
|
||||
Self::Ephemeral(Arc::new(store))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ElasticSearchStore> for SearchStore {
|
||||
fn from(store: ElasticSearchStore) -> Self {
|
||||
Self::ElasticSearch(Arc::new(store))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MeiliSearchStore> for SearchStore {
|
||||
fn from(store: MeiliSearchStore) -> Self {
|
||||
Self::MeiliSearch(Arc::new(store))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "redis")]
|
||||
impl From<backend::redis::RedisStore> for InMemoryStore {
|
||||
fn from(store: backend::redis::RedisStore) -> Self {
|
||||
Self::Redis(Arc::new(store))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Store> for SearchStore {
|
||||
fn from(store: Store) -> Self {
|
||||
Self::Store(store)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Store> for InMemoryStore {
|
||||
fn from(store: Store) -> Self {
|
||||
Self::Store(store)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Store> for BlobStore {
|
||||
fn from(store: Store) -> Self {
|
||||
Self::Store(store)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BlobStore {
|
||||
fn default() -> Self {
|
||||
Self::Store(Store::None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryStore {
|
||||
fn default() -> Self {
|
||||
Self::Store(Store::None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SearchStore {
|
||||
fn default() -> Self {
|
||||
Self::Store(Store::None)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Value<'x> {
|
||||
Integer(i64),
|
||||
Bool(bool),
|
||||
Float(f64),
|
||||
Text(Cow<'x, str>),
|
||||
Blob(Cow<'x, [u8]>),
|
||||
Null,
|
||||
}
|
||||
|
||||
impl Eq for Value<'_> {}
|
||||
|
||||
impl<'x> Value<'x> {
|
||||
pub fn to_str<'y: 'x>(&'y self) -> Cow<'x, str> {
|
||||
match self {
|
||||
Value::Text(s) => s.as_ref().into(),
|
||||
Value::Integer(i) => Cow::Owned(i.to_string()),
|
||||
Value::Bool(b) => Cow::Owned(b.to_string()),
|
||||
Value::Float(f) => Cow::Owned(f.to_string()),
|
||||
Value::Blob(b) => String::from_utf8_lossy(b.as_ref()),
|
||||
Value::Null => Cow::Borrowed(""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Row {
|
||||
pub values: Vec<Value<'static>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Rows {
|
||||
pub rows: Vec<Row>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NamedRows {
|
||||
pub names: Vec<String>,
|
||||
pub rows: Vec<Row>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum QueryType {
|
||||
Execute,
|
||||
Exists,
|
||||
QueryAll,
|
||||
QueryOne,
|
||||
}
|
||||
|
||||
pub trait QueryResult: Sync + Send + 'static {
|
||||
fn from_exec(items: usize) -> Self;
|
||||
fn from_exists(exists: bool) -> Self;
|
||||
fn from_query_one(items: impl IntoRows) -> Self;
|
||||
fn from_query_all(items: impl IntoRows) -> Self;
|
||||
|
||||
fn query_type() -> QueryType;
|
||||
}
|
||||
|
||||
pub trait IntoRows {
|
||||
fn into_row(self) -> Option<Row>;
|
||||
fn into_rows(self) -> Rows;
|
||||
fn into_named_rows(self) -> NamedRows;
|
||||
}
|
||||
|
||||
impl QueryResult for Option<Row> {
|
||||
fn query_type() -> QueryType {
|
||||
QueryType::QueryOne
|
||||
}
|
||||
|
||||
fn from_exec(_: usize) -> Self {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn from_exists(_: bool) -> Self {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn from_query_all(_: impl IntoRows) -> Self {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn from_query_one(items: impl IntoRows) -> Self {
|
||||
items.into_row()
|
||||
}
|
||||
}
|
||||
|
||||
impl QueryResult for Rows {
|
||||
fn query_type() -> QueryType {
|
||||
QueryType::QueryAll
|
||||
}
|
||||
|
||||
fn from_exec(_: usize) -> Self {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn from_exists(_: bool) -> Self {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn from_query_all(items: impl IntoRows) -> Self {
|
||||
items.into_rows()
|
||||
}
|
||||
|
||||
fn from_query_one(_: impl IntoRows) -> Self {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl QueryResult for NamedRows {
|
||||
fn query_type() -> QueryType {
|
||||
QueryType::QueryAll
|
||||
}
|
||||
|
||||
fn from_exec(_: usize) -> Self {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn from_exists(_: bool) -> Self {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn from_query_all(items: impl IntoRows) -> Self {
|
||||
items.into_named_rows()
|
||||
}
|
||||
|
||||
fn from_query_one(_: impl IntoRows) -> Self {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl QueryResult for bool {
|
||||
fn query_type() -> QueryType {
|
||||
QueryType::Exists
|
||||
}
|
||||
|
||||
fn from_exec(_: usize) -> Self {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn from_exists(exists: bool) -> Self {
|
||||
exists
|
||||
}
|
||||
|
||||
fn from_query_all(_: impl IntoRows) -> Self {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn from_query_one(_: impl IntoRows) -> Self {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl QueryResult for usize {
|
||||
fn query_type() -> QueryType {
|
||||
QueryType::Execute
|
||||
}
|
||||
|
||||
fn from_exec(items: usize) -> Self {
|
||||
items
|
||||
}
|
||||
|
||||
fn from_exists(_: bool) -> Self {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn from_query_all(_: impl IntoRows) -> Self {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn from_query_one(_: impl IntoRows) -> Self {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<&'x str> for Value<'x> {
|
||||
fn from(value: &'x str) -> Self {
|
||||
Self::Text(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Value<'_> {
|
||||
fn from(value: String) -> Self {
|
||||
Self::Text(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<&'x String> for Value<'x> {
|
||||
fn from(value: &'x String) -> Self {
|
||||
Self::Text(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<Cow<'x, str>> for Value<'x> {
|
||||
fn from(value: Cow<'x, str>) -> Self {
|
||||
Self::Text(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Value<'_> {
|
||||
fn from(value: bool) -> Self {
|
||||
Self::Bool(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for Value<'_> {
|
||||
fn from(value: i64) -> Self {
|
||||
Self::Integer(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Value<'static>> for i64 {
|
||||
fn from(value: Value<'static>) -> Self {
|
||||
if let Value::Integer(value) = value {
|
||||
value
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for Value<'_> {
|
||||
fn from(value: u64) -> Self {
|
||||
Self::Integer(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Value<'_> {
|
||||
fn from(value: u32) -> Self {
|
||||
Self::Integer(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Value<'_> {
|
||||
fn from(value: f64) -> Self {
|
||||
Self::Float(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<&'x [u8]> for Value<'x> {
|
||||
fn from(value: &'x [u8]) -> Self {
|
||||
Self::Blob(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for Value<'_> {
|
||||
fn from(value: Vec<u8>) -> Self {
|
||||
Self::Blob(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Value<'_> {
|
||||
pub fn into_string(self) -> String {
|
||||
match self {
|
||||
Value::Text(s) => s.into_owned(),
|
||||
Value::Integer(i) => i.to_string(),
|
||||
Value::Bool(b) => b.to_string(),
|
||||
Value::Float(f) => f.to_string(),
|
||||
Value::Blob(b) => String::from_utf8_lossy(b.as_ref()).into_owned(),
|
||||
Value::Null => "".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_lower_string(self) -> String {
|
||||
match self {
|
||||
Value::Text(s) => s.as_ref().to_lowercase(),
|
||||
Value::Integer(i) => i.to_string(),
|
||||
Value::Bool(b) => b.to_string(),
|
||||
Value::Float(f) => f.to_string(),
|
||||
Value::Blob(b) => String::from_utf8_lossy(b.as_ref()).to_lowercase(),
|
||||
Value::Null => "".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Row> for Vec<String> {
|
||||
fn from(value: Row) -> Self {
|
||||
value.values.into_iter().map(|v| v.into_string()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Row> for Vec<u32> {
|
||||
fn from(value: Row) -> Self {
|
||||
value
|
||||
.values
|
||||
.into_iter()
|
||||
.filter_map(|v| {
|
||||
if let Value::Integer(v) = v {
|
||||
Some(v as u32)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rows> for Vec<String> {
|
||||
fn from(value: Rows) -> Self {
|
||||
value
|
||||
.rows
|
||||
.into_iter()
|
||||
.flat_map(|v| v.values.into_iter().map(|v| v.into_string()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rows> for Vec<u32> {
|
||||
fn from(value: Rows) -> Self {
|
||||
value
|
||||
.rows
|
||||
.into_iter()
|
||||
.flat_map(|v| {
|
||||
v.values.into_iter().filter_map(|v| {
|
||||
if let Value::Integer(v) = v {
|
||||
Some(v as u32)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Store {
|
||||
#[inline(always)]
|
||||
pub fn is_none(&self) -> bool {
|
||||
matches!(self, Self::None)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_active(&self) -> bool {
|
||||
!matches!(self, Self::None)
|
||||
}
|
||||
|
||||
pub fn is_same(&self, other: &Store) -> bool {
|
||||
match (self, other) {
|
||||
#[cfg(feature = "sqlite")]
|
||||
(Store::SQLite(a), Store::SQLite(b)) => Arc::ptr_eq(a, b),
|
||||
#[cfg(feature = "foundation")]
|
||||
(Store::FoundationDb(a), Store::FoundationDb(b)) => Arc::ptr_eq(a, b),
|
||||
#[cfg(feature = "postgres")]
|
||||
(Store::PostgreSQL(a), Store::PostgreSQL(b)) => Arc::ptr_eq(a, b),
|
||||
#[cfg(feature = "mysql")]
|
||||
(Store::MySQL(a), Store::MySQL(b)) => Arc::ptr_eq(a, b),
|
||||
#[cfg(feature = "rocks")]
|
||||
(Store::RocksDb(a), Store::RocksDb(b)) => Arc::ptr_eq(a, b),
|
||||
(Store::Ephemeral(a), Store::Ephemeral(b)) => Arc::ptr_eq(a, b),
|
||||
#[cfg(all(feature = "enterprise", any(feature = "postgres", feature = "mysql")))]
|
||||
(Store::SQLReadReplica(a), Store::SQLReadReplica(b)) => Arc::ptr_eq(a, b),
|
||||
(Store::None, Store::None) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_sql(&self) -> bool {
|
||||
match self {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Store::SQLite(_) => true,
|
||||
#[cfg(feature = "postgres")]
|
||||
Store::PostgreSQL(_) => true,
|
||||
#[cfg(feature = "mysql")]
|
||||
Store::MySQL(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_pg_or_mysql(&self) -> bool {
|
||||
match self {
|
||||
#[cfg(feature = "mysql")]
|
||||
Store::MySQL(_) => true,
|
||||
#[cfg(feature = "postgres")]
|
||||
Store::PostgreSQL(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_foundationdb(&self) -> bool {
|
||||
match self {
|
||||
#[cfg(feature = "foundation")]
|
||||
Store::FoundationDb(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_ephemeral(&self) -> bool {
|
||||
matches!(self, Self::Ephemeral(_))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Store {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Self::SQLite(_) => f.debug_tuple("SQLite").finish(),
|
||||
#[cfg(feature = "foundation")]
|
||||
Self::FoundationDb(_) => f.debug_tuple("FoundationDb").finish(),
|
||||
#[cfg(feature = "postgres")]
|
||||
Self::PostgreSQL(_) => f.debug_tuple("PostgreSQL").finish(),
|
||||
#[cfg(feature = "mysql")]
|
||||
Self::MySQL(_) => f.debug_tuple("MySQL").finish(),
|
||||
#[cfg(feature = "rocks")]
|
||||
Self::RocksDb(_) => f.debug_tuple("RocksDb").finish(),
|
||||
Self::Ephemeral(_) => f.debug_tuple("Ephemeral").finish(),
|
||||
|
||||
Self::None => f.debug_tuple("None").finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Value<'_>> for trc::Value {
|
||||
fn from(value: Value) -> Self {
|
||||
match value {
|
||||
Value::Integer(v) => trc::Value::Int(v),
|
||||
Value::Bool(v) => trc::Value::Bool(v),
|
||||
Value::Float(v) => trc::Value::Float(v),
|
||||
Value::Text(v) => trc::Value::String(match v {
|
||||
Cow::Borrowed(v) => v.into(),
|
||||
Cow::Owned(v) => v.into(),
|
||||
}),
|
||||
Value::Blob(v) => trc::Value::Bytes(v.into_owned()),
|
||||
Value::Null => trc::Value::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Value<'static>> for () {
|
||||
fn from(_: Value<'static>) -> Self {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use ahash::AHashSet;
|
||||
use trc::AddContext;
|
||||
use types::collection::Collection;
|
||||
|
||||
use crate::{
|
||||
Deserialize, IterateParams, Store, U32_LEN, ValueKey,
|
||||
write::{BatchBuilder, ValueClass, key::DeserializeBigEndian},
|
||||
};
|
||||
|
||||
pub enum AclQuery {
|
||||
SharedWith {
|
||||
grant_account_id: u32,
|
||||
to_account_id: u32,
|
||||
to_collection: u8,
|
||||
},
|
||||
HasAccess {
|
||||
grant_account_id: u32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AclItem {
|
||||
pub to_account_id: u32,
|
||||
pub to_collection: Collection,
|
||||
pub to_document_id: u32,
|
||||
pub permissions: u64,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
pub async fn acl_query(&self, query: AclQuery) -> trc::Result<Vec<AclItem>> {
|
||||
let mut results = Vec::new();
|
||||
let (from_key, to_key) = match query {
|
||||
AclQuery::SharedWith {
|
||||
grant_account_id,
|
||||
to_account_id,
|
||||
to_collection,
|
||||
} => {
|
||||
let from_key = ValueKey {
|
||||
account_id: to_account_id,
|
||||
collection: to_collection,
|
||||
document_id: 0,
|
||||
class: ValueClass::Acl(grant_account_id),
|
||||
};
|
||||
let mut to_key = from_key.clone();
|
||||
to_key.document_id = u32::MAX;
|
||||
|
||||
(from_key, to_key)
|
||||
}
|
||||
AclQuery::HasAccess { grant_account_id } => (
|
||||
ValueKey {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
class: ValueClass::Acl(grant_account_id),
|
||||
},
|
||||
ValueKey {
|
||||
account_id: u32::MAX,
|
||||
collection: u8::MAX,
|
||||
document_id: u32::MAX,
|
||||
class: ValueClass::Acl(grant_account_id),
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
self.iterate(
|
||||
IterateParams::new(from_key, to_key).ascending(),
|
||||
|key, value| {
|
||||
results.push(AclItem::deserialize(key)?.with_permissions(u64::deserialize(value)?));
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())
|
||||
.map(|_| results)
|
||||
}
|
||||
|
||||
pub async fn acl_revoke_all(&self, account_id: u32) -> trc::Result<AHashSet<u32>> {
|
||||
let from_key = ValueKey {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
class: ValueClass::Acl(0),
|
||||
};
|
||||
let to_key = ValueKey {
|
||||
account_id: u32::MAX,
|
||||
collection: u8::MAX,
|
||||
document_id: u32::MAX,
|
||||
class: ValueClass::Acl(u32::MAX),
|
||||
};
|
||||
|
||||
let mut delete_keys = Vec::new();
|
||||
let mut revoked_accounts = AHashSet::new();
|
||||
self.iterate(
|
||||
IterateParams::new(from_key, to_key).ascending().no_values(),
|
||||
|key, _| {
|
||||
if account_id == key.deserialize_be_u32(U32_LEN)? {
|
||||
let owner_account_id = key.deserialize_be_u32(0)?;
|
||||
revoked_accounts.insert(owner_account_id);
|
||||
delete_keys.push((owner_account_id, AclItem::deserialize(key)?));
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Remove permissions
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch.with_account_id(account_id);
|
||||
let mut last_collection = Collection::None;
|
||||
for (revoke_account_id, acl_item) in delete_keys.into_iter() {
|
||||
if batch.is_large_batch() {
|
||||
self.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
batch = BatchBuilder::new();
|
||||
batch.with_account_id(account_id);
|
||||
last_collection = Collection::None;
|
||||
}
|
||||
if acl_item.to_collection != last_collection {
|
||||
batch.with_collection(acl_item.to_collection);
|
||||
last_collection = acl_item.to_collection;
|
||||
}
|
||||
batch
|
||||
.with_document(acl_item.to_document_id)
|
||||
.acl_revoke(revoke_account_id);
|
||||
}
|
||||
if !batch.is_empty() {
|
||||
self.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
Ok(revoked_accounts)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for AclItem {
|
||||
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
|
||||
Ok(AclItem {
|
||||
to_account_id: bytes.deserialize_be_u32(U32_LEN)?,
|
||||
to_collection: bytes
|
||||
.get(U32_LEN * 2)
|
||||
.map(|b| Collection::from(*b))
|
||||
.ok_or_else(|| trc::StoreEvent::DataCorruption.caused_by(trc::location!()))?,
|
||||
to_document_id: bytes.deserialize_be_u32((U32_LEN * 2) + 1)?,
|
||||
permissions: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AclItem {
|
||||
fn with_permissions(mut self, permissions: u64) -> Self {
|
||||
self.permissions = permissions;
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use trc::AddContext;
|
||||
use types::collection::{SyncCollection, VanishedCollection};
|
||||
use utils::codec::leb128::Leb128Iterator;
|
||||
|
||||
use crate::{
|
||||
IterateParams, LogKey, Store, U32_LEN, U64_LEN,
|
||||
write::{LogCollection, key::DeserializeBigEndian},
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum Change {
|
||||
InsertContainer(u64),
|
||||
UpdateContainer(u64),
|
||||
UpdateContainerProperty(u64),
|
||||
DeleteContainer(u64),
|
||||
InsertItem(u64),
|
||||
UpdateItem(u64),
|
||||
DeleteItem(u64),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Changes {
|
||||
pub changes: Vec<Change>,
|
||||
pub from_change_id: u64,
|
||||
pub to_change_id: u64,
|
||||
pub container_change_id: Option<u64>,
|
||||
pub item_change_id: Option<u64>,
|
||||
pub is_truncated: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Query {
|
||||
All,
|
||||
Since(u64),
|
||||
SinceInclusive(u64),
|
||||
RangeInclusive(u64, u64),
|
||||
}
|
||||
|
||||
pub trait DeserializeVanished: Sized + Sync + Send {
|
||||
fn deserialize_vanished<'x>(bytes: &mut impl Iterator<Item = &'x u8>) -> Option<Self>;
|
||||
}
|
||||
|
||||
impl Default for Changes {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
changes: Vec::with_capacity(10),
|
||||
from_change_id: 0,
|
||||
to_change_id: 0,
|
||||
container_change_id: None,
|
||||
item_change_id: None,
|
||||
is_truncated: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Store {
|
||||
pub async fn changes(
|
||||
&self,
|
||||
account_id: u32,
|
||||
collection_: LogCollection,
|
||||
query: Query,
|
||||
) -> trc::Result<Changes> {
|
||||
let is_share_log = matches!(
|
||||
collection_,
|
||||
LogCollection::Sync(SyncCollection::ShareNotification)
|
||||
);
|
||||
let collection = u8::from(collection_);
|
||||
|
||||
let (is_inclusive, from_change_id, to_change_id) = match query {
|
||||
Query::All => (true, 0, u64::MAX),
|
||||
Query::Since(change_id) => (false, change_id, u64::MAX),
|
||||
Query::SinceInclusive(change_id) => (true, change_id, u64::MAX),
|
||||
Query::RangeInclusive(from_change_id, to_change_id) => {
|
||||
(true, from_change_id, to_change_id)
|
||||
}
|
||||
};
|
||||
let from_key = LogKey {
|
||||
account_id,
|
||||
collection,
|
||||
change_id: from_change_id,
|
||||
};
|
||||
let to_key = LogKey {
|
||||
account_id,
|
||||
collection,
|
||||
change_id: to_change_id,
|
||||
};
|
||||
|
||||
let mut changelog = Changes::default();
|
||||
|
||||
self.iterate(
|
||||
IterateParams::new(from_key, to_key).ascending(),
|
||||
|key, value| {
|
||||
let change_id = key.deserialize_be_u64(key.len() - U64_LEN)?;
|
||||
if is_inclusive || change_id != from_change_id {
|
||||
if value.is_empty() {
|
||||
changelog.is_truncated = true;
|
||||
return Ok(true);
|
||||
}
|
||||
if changelog.changes.is_empty() {
|
||||
changelog.from_change_id = change_id;
|
||||
}
|
||||
changelog.to_change_id = change_id;
|
||||
if !is_share_log {
|
||||
let (has_container_changes, has_item_changes) =
|
||||
changelog.deserialize(value).ok_or_else(|| {
|
||||
trc::Error::corrupted_key(key, value.into(), trc::location!())
|
||||
})?;
|
||||
if has_container_changes {
|
||||
changelog.container_change_id = Some(change_id);
|
||||
}
|
||||
if has_item_changes {
|
||||
changelog.item_change_id = Some(change_id);
|
||||
}
|
||||
} else {
|
||||
changelog.changes.push(Change::InsertItem(change_id));
|
||||
}
|
||||
} else {
|
||||
changelog.from_change_id = change_id;
|
||||
changelog.to_change_id = change_id;
|
||||
}
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(changelog)
|
||||
}
|
||||
|
||||
pub async fn vanished<T: DeserializeVanished>(
|
||||
&self,
|
||||
account_id: u32,
|
||||
collection: LogCollection,
|
||||
query: Query,
|
||||
) -> trc::Result<Vec<T>> {
|
||||
let collection = u8::from(collection);
|
||||
let (is_inclusive, from_change_id, to_change_id) = match query {
|
||||
Query::All => (true, 0, u64::MAX),
|
||||
Query::Since(change_id) => (false, change_id, u64::MAX),
|
||||
Query::SinceInclusive(change_id) => (true, change_id, u64::MAX),
|
||||
Query::RangeInclusive(from_change_id, to_change_id) => {
|
||||
(true, from_change_id, to_change_id)
|
||||
}
|
||||
};
|
||||
let from_key = LogKey {
|
||||
account_id,
|
||||
collection,
|
||||
change_id: from_change_id,
|
||||
};
|
||||
let to_key = LogKey {
|
||||
account_id,
|
||||
collection,
|
||||
change_id: to_change_id,
|
||||
};
|
||||
|
||||
let mut vanished = Vec::default();
|
||||
|
||||
self.iterate(
|
||||
IterateParams::new(from_key, to_key).ascending(),
|
||||
|key, value| {
|
||||
let change_id = key.deserialize_be_u64(key.len() - U64_LEN)?;
|
||||
if is_inclusive || change_id != from_change_id {
|
||||
let mut iter = value.iter().peekable();
|
||||
|
||||
while iter.peek().is_some() {
|
||||
if let Some(item) = T::deserialize_vanished(&mut iter) {
|
||||
vanished.push(item);
|
||||
} else {
|
||||
return Err(trc::Error::corrupted_key(
|
||||
key,
|
||||
value.into(),
|
||||
trc::location!(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(vanished)
|
||||
}
|
||||
|
||||
pub async fn get_last_change_id(
|
||||
&self,
|
||||
account_id: u32,
|
||||
collection: LogCollection,
|
||||
) -> trc::Result<Option<u64>> {
|
||||
let collection = u8::from(collection);
|
||||
let from_key = LogKey {
|
||||
account_id,
|
||||
collection,
|
||||
change_id: 0,
|
||||
};
|
||||
let to_key = LogKey {
|
||||
account_id,
|
||||
collection,
|
||||
change_id: u64::MAX,
|
||||
};
|
||||
|
||||
let mut last_change_id = None;
|
||||
|
||||
self.iterate(
|
||||
IterateParams::new(from_key, to_key)
|
||||
.descending()
|
||||
.no_values()
|
||||
.only_first(),
|
||||
|key, _| {
|
||||
last_change_id = key.deserialize_be_u64(key.len() - U64_LEN)?.into();
|
||||
Ok(false)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(last_change_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VanishedCollection> for LogCollection {
|
||||
fn from(value: VanishedCollection) -> Self {
|
||||
LogCollection::Vanished(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SyncCollection> for LogCollection {
|
||||
fn from(value: SyncCollection) -> Self {
|
||||
LogCollection::Sync(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Changes {
|
||||
pub fn deserialize(&mut self, bytes: &[u8]) -> Option<(bool, bool)> {
|
||||
let mut bytes_it = bytes.iter();
|
||||
|
||||
let container_inserts: usize = bytes_it.next_leb128()?;
|
||||
let container_updates: usize = bytes_it.next_leb128()?;
|
||||
let container_property_changes: usize = bytes_it.next_leb128()?;
|
||||
let container_deletes: usize = bytes_it.next_leb128()?;
|
||||
|
||||
let item_inserts: usize = bytes_it.next_leb128()?;
|
||||
let item_updates: usize = bytes_it.next_leb128()?;
|
||||
let item_deletes: usize = bytes_it.next_leb128()?;
|
||||
|
||||
let has_container_changes =
|
||||
container_inserts + container_updates + container_property_changes + container_deletes
|
||||
> 0;
|
||||
let has_item_changes = item_inserts + item_updates + item_deletes > 0;
|
||||
|
||||
if container_inserts > 0 {
|
||||
for _ in 0..container_inserts {
|
||||
self.changes
|
||||
.push(Change::InsertContainer(bytes_it.next_leb128()?));
|
||||
}
|
||||
}
|
||||
|
||||
if container_updates > 0 || container_property_changes > 0 {
|
||||
'update_outer: for change_pos in 0..(container_updates + container_property_changes) {
|
||||
let id = bytes_it.next_leb128()?;
|
||||
let mut is_property_change = change_pos >= container_updates;
|
||||
|
||||
for (idx, change) in self.changes.iter().enumerate() {
|
||||
match change {
|
||||
Change::InsertContainer(insert_id) if *insert_id == id => {
|
||||
// Item updated after inserted, no need to count this change.
|
||||
continue 'update_outer;
|
||||
}
|
||||
Change::UpdateContainer(update_id) if *update_id == id => {
|
||||
// Move update to the front
|
||||
is_property_change = false;
|
||||
self.changes.remove(idx);
|
||||
break;
|
||||
}
|
||||
Change::UpdateContainerProperty(update_id) if *update_id == id => {
|
||||
// Move update to the front
|
||||
self.changes.remove(idx);
|
||||
break;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
self.changes.push(if !is_property_change {
|
||||
Change::UpdateContainer(id)
|
||||
} else {
|
||||
Change::UpdateContainerProperty(id)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if container_deletes > 0 {
|
||||
'delete_outer: for _ in 0..container_deletes {
|
||||
let id = bytes_it.next_leb128()?;
|
||||
|
||||
'delete_inner: for (idx, change) in self.changes.iter().enumerate() {
|
||||
match change {
|
||||
Change::InsertContainer(insert_id) if *insert_id == id => {
|
||||
self.changes.remove(idx);
|
||||
continue 'delete_outer;
|
||||
}
|
||||
Change::UpdateContainer(update_id) if *update_id == id => {
|
||||
self.changes.remove(idx);
|
||||
break 'delete_inner;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
self.changes.push(Change::DeleteContainer(id));
|
||||
}
|
||||
}
|
||||
|
||||
// Item changes
|
||||
if item_inserts > 0 {
|
||||
for _ in 0..item_inserts {
|
||||
self.changes
|
||||
.push(Change::InsertItem(bytes_it.next_leb128()?));
|
||||
}
|
||||
}
|
||||
|
||||
if item_updates > 0 {
|
||||
'update_outer: for _ in 0..item_updates {
|
||||
let id = bytes_it.next_leb128()?;
|
||||
|
||||
for (idx, change) in self.changes.iter().enumerate() {
|
||||
match change {
|
||||
Change::InsertItem(insert_id) if *insert_id == id => {
|
||||
// Item updated after inserted, no need to count this change.
|
||||
continue 'update_outer;
|
||||
}
|
||||
Change::UpdateItem(update_id) if *update_id == id => {
|
||||
// Move update to the front
|
||||
self.changes.remove(idx);
|
||||
break;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
self.changes.push(Change::UpdateItem(id));
|
||||
}
|
||||
}
|
||||
|
||||
if item_deletes > 0 {
|
||||
'delete_outer: for _ in 0..item_deletes {
|
||||
let id = bytes_it.next_leb128()?;
|
||||
|
||||
'delete_inner: for (idx, change) in self.changes.iter().enumerate() {
|
||||
match change {
|
||||
Change::InsertItem(insert_id) if *insert_id == id => {
|
||||
self.changes.remove(idx);
|
||||
continue 'delete_outer;
|
||||
}
|
||||
Change::UpdateItem(update_id) if *update_id == id => {
|
||||
self.changes.remove(idx);
|
||||
break 'delete_inner;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
self.changes.push(Change::DeleteItem(id));
|
||||
}
|
||||
}
|
||||
|
||||
Some((has_container_changes, has_item_changes))
|
||||
}
|
||||
}
|
||||
|
||||
impl Changes {
|
||||
pub fn total_container_changes(&self) -> usize {
|
||||
self.changes
|
||||
.iter()
|
||||
.filter(|change| change.is_container_change())
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn total_item_changes(&self) -> usize {
|
||||
self.changes
|
||||
.iter()
|
||||
.filter(|change| change.is_item_change())
|
||||
.count()
|
||||
}
|
||||
}
|
||||
|
||||
impl Change {
|
||||
pub fn item_id(&self) -> Option<u64> {
|
||||
match self {
|
||||
Change::InsertItem(id) => Some(*id),
|
||||
Change::UpdateItem(id) => Some(*id),
|
||||
Change::DeleteItem(id) => Some(*id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn container_id(&self) -> Option<u64> {
|
||||
match self {
|
||||
Change::InsertContainer(id) => Some(*id),
|
||||
Change::UpdateContainer(id) => Some(*id),
|
||||
Change::UpdateContainerProperty(id) => Some(*id),
|
||||
Change::DeleteContainer(id) => Some(*id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_unwrap_item_id(self) -> Option<u64> {
|
||||
match self {
|
||||
Change::InsertItem(id) => Some(id),
|
||||
Change::UpdateItem(id) => Some(id),
|
||||
Change::DeleteItem(id) => Some(id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_unwrap_container_id(self) -> Option<u64> {
|
||||
match self {
|
||||
Change::InsertContainer(id) => Some(id),
|
||||
Change::UpdateContainer(id) => Some(id),
|
||||
Change::UpdateContainerProperty(id) => Some(id),
|
||||
Change::DeleteContainer(id) => Some(id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_container_change(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Change::InsertContainer(_)
|
||||
| Change::UpdateContainer(_)
|
||||
| Change::UpdateContainerProperty(_)
|
||||
| Change::DeleteContainer(_)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_item_change(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Change::InsertItem(_) | Change::UpdateItem(_) | Change::DeleteItem(_)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl DeserializeVanished for u64 {
|
||||
fn deserialize_vanished<'x>(bytes: &mut impl Iterator<Item = &'x u8>) -> Option<Self> {
|
||||
let mut num = [0u8; U64_LEN];
|
||||
for i in num.iter_mut() {
|
||||
*i = *bytes.next()?;
|
||||
}
|
||||
Some(u64::from_be_bytes(num))
|
||||
}
|
||||
}
|
||||
|
||||
impl DeserializeVanished for (u32, u32) {
|
||||
fn deserialize_vanished<'x>(bytes: &mut impl Iterator<Item = &'x u8>) -> Option<Self> {
|
||||
let mut num1 = [0u8; U32_LEN];
|
||||
let mut num2 = [0u8; U32_LEN];
|
||||
for i in num1.iter_mut().chain(num2.iter_mut()) {
|
||||
*i = *bytes.next()?;
|
||||
}
|
||||
Some((u32::from_be_bytes(num1), u32::from_be_bytes(num2)))
|
||||
}
|
||||
}
|
||||
|
||||
impl DeserializeVanished for String {
|
||||
fn deserialize_vanished<'x>(bytes: &mut impl Iterator<Item = &'x u8>) -> Option<Self> {
|
||||
let mut name = Vec::with_capacity(16);
|
||||
|
||||
loop {
|
||||
let byte = bytes.next()?;
|
||||
if *byte != 0 {
|
||||
name.push(*byte);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
String::from_utf8(name).ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod acl;
|
||||
pub mod log;
|
||||
|
||||
use crate::{IterateParams, Key};
|
||||
|
||||
impl<T: Key> IterateParams<T> {
|
||||
pub fn new(begin: T, end: T) -> Self {
|
||||
IterateParams {
|
||||
begin,
|
||||
end,
|
||||
first: false,
|
||||
ascending: true,
|
||||
values: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_ascending(mut self, ascending: bool) -> Self {
|
||||
self.ascending = ascending;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_values(mut self, values: bool) -> Self {
|
||||
self.values = values;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ascending(mut self) -> Self {
|
||||
self.ascending = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn descending(mut self) -> Self {
|
||||
self.ascending = false;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn only_first(mut self) -> Self {
|
||||
self.first = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn no_values(mut self) -> Self {
|
||||
self.values = false;
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{RegistryStore, Store, registry::RegistryObject};
|
||||
use registry::{
|
||||
schema::{
|
||||
prelude::{Object, ObjectType, Property},
|
||||
structs::ClusterRole,
|
||||
},
|
||||
types::{
|
||||
ObjectImpl,
|
||||
error::{Error, ValidationError, Warning},
|
||||
id::ObjectId,
|
||||
},
|
||||
};
|
||||
use types::id::Id;
|
||||
|
||||
pub struct Bootstrap {
|
||||
pub registry: RegistryStore,
|
||||
pub data_store: Store,
|
||||
pub errors: Vec<Error>,
|
||||
pub warnings: Vec<Warning>,
|
||||
pub has_fatal_errors: bool,
|
||||
pub role: Option<ClusterRole>,
|
||||
}
|
||||
|
||||
impl Bootstrap {
|
||||
pub async fn new(registry: RegistryStore) -> Self {
|
||||
let mut bp = Self::new_uninitialized(registry);
|
||||
|
||||
let Some(role_name) = bp.registry.cluster_role().map(|r| r.to_string()) else {
|
||||
return bp;
|
||||
};
|
||||
|
||||
for role in bp.list_infallible::<ClusterRole>().await {
|
||||
if role.object.name == role_name {
|
||||
bp.role = Some(role.object);
|
||||
return bp;
|
||||
}
|
||||
}
|
||||
|
||||
bp.build_error(
|
||||
ObjectType::ClusterRole.singleton(),
|
||||
format!("Cluster role \"{role_name}\" not found in registry"),
|
||||
);
|
||||
|
||||
bp
|
||||
}
|
||||
|
||||
pub fn new_uninitialized(registry: RegistryStore) -> Self {
|
||||
Self {
|
||||
data_store: registry.0.store.clone(),
|
||||
registry,
|
||||
errors: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
has_fatal_errors: false,
|
||||
role: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_data_store(mut self, data_store: Store) -> Self {
|
||||
self.data_store = data_store;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn setting<T: ObjectImpl + From<Object>>(&mut self) -> trc::Result<T> {
|
||||
let object_id = T::OBJECT.singleton();
|
||||
|
||||
if let Some(setting) = self.registry.object::<T>(object_id.id()).await? {
|
||||
let mut errors = Vec::new();
|
||||
if setting.validate(&mut errors) {
|
||||
return Ok(setting);
|
||||
}
|
||||
self.errors.push(Error::Validation { object_id, errors });
|
||||
}
|
||||
|
||||
Ok(T::default())
|
||||
}
|
||||
|
||||
pub async fn setting_infallible<T: ObjectImpl + From<Object>>(&mut self) -> T {
|
||||
match self.setting::<T>().await {
|
||||
Ok(setting) => setting,
|
||||
Err(err) => {
|
||||
if !self.has_fatal_errors {
|
||||
self.errors.push(Error::Internal {
|
||||
object_id: Some(T::OBJECT.singleton()),
|
||||
error: err,
|
||||
});
|
||||
self.has_fatal_errors = true;
|
||||
}
|
||||
T::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_infallible<T: ObjectImpl + From<Object>>(&mut self, id: Id) -> Option<T> {
|
||||
match self.registry.object::<T>(id).await {
|
||||
Ok(Some(setting)) => {
|
||||
let mut errors = Vec::new();
|
||||
if setting.validate(&mut errors) {
|
||||
Some(setting)
|
||||
} else {
|
||||
self.errors.push(Error::Validation {
|
||||
object_id: ObjectId::new(T::OBJECT, id),
|
||||
errors,
|
||||
});
|
||||
None
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
self.errors.push(Error::NotFound {
|
||||
object_id: ObjectId::new(T::OBJECT, id),
|
||||
});
|
||||
None
|
||||
}
|
||||
Err(err) => {
|
||||
if !self.has_fatal_errors {
|
||||
self.errors.push(Error::Internal {
|
||||
object_id: Some(ObjectId::new(T::OBJECT, id)),
|
||||
error: err,
|
||||
});
|
||||
self.has_fatal_errors = true;
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_infallible<T: ObjectImpl + From<Object>>(
|
||||
&mut self,
|
||||
) -> Vec<RegistryObject<T>> {
|
||||
match self.registry.list::<T>().await {
|
||||
Ok(objects) => objects
|
||||
.into_iter()
|
||||
.filter(|object| self.validate(object.id, &object.object))
|
||||
.collect(),
|
||||
Err(err) => {
|
||||
if !self.has_fatal_errors {
|
||||
self.errors.push(Error::Internal {
|
||||
object_id: None,
|
||||
error: err,
|
||||
});
|
||||
self.has_fatal_errors = true;
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_error(&mut self, id: ObjectId, message: impl Into<String>) {
|
||||
self.errors.push(Error::Build {
|
||||
object_id: id,
|
||||
message: message.into(),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn build_warning(&mut self, id: ObjectId, message: impl Into<String>) {
|
||||
self.warnings.push(Warning {
|
||||
object_id: id,
|
||||
property: None,
|
||||
message: message.into(),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn invalid_property(&mut self, id: ObjectId, property: Property, value: impl Into<String>) {
|
||||
self.errors.push(Error::Validation {
|
||||
object_id: id,
|
||||
errors: vec![ValidationError::Invalid {
|
||||
property,
|
||||
value: value.into(),
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
pub fn validate(&mut self, id: ObjectId, object: &impl ObjectImpl) -> bool {
|
||||
let mut errors = Vec::new();
|
||||
if object.validate(&mut errors) {
|
||||
true
|
||||
} else {
|
||||
self.errors.push(Error::Validation {
|
||||
object_id: id,
|
||||
errors,
|
||||
});
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn node_id(&self) -> u16 {
|
||||
self.registry.0.node_id
|
||||
}
|
||||
|
||||
pub fn log_errors(&self) {
|
||||
for error in &self.errors {
|
||||
error.log();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_warnings(&self) {
|
||||
for warning in &self.warnings {
|
||||
warning.log();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
IterateParams, RegistryStore, SUBSPACE_REGISTRY, U16_LEN, U64_LEN, ValueKey,
|
||||
registry::{RegistryObject, local::RegistryInit},
|
||||
write::{
|
||||
AnyClass, RegistryClass, ValueClass,
|
||||
key::{DeserializeBigEndian, KeySerializer},
|
||||
},
|
||||
};
|
||||
use registry::{
|
||||
pickle::PickledStream,
|
||||
schema::prelude::{Object, ObjectType},
|
||||
types::{EnumImpl, ObjectImpl, id::ObjectId},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::id::Id;
|
||||
|
||||
impl RegistryStore {
|
||||
pub async fn get(&self, object_id: ObjectId) -> trc::Result<Option<Object>> {
|
||||
if object_id.object() != ObjectType::DataStore {
|
||||
self.0
|
||||
.store
|
||||
.get_value::<Object>(ValueKey::from(ValueClass::Registry(RegistryClass::Item {
|
||||
object_id: object_id.object().to_id(),
|
||||
item_id: object_id.id().id(),
|
||||
})))
|
||||
.await
|
||||
} else {
|
||||
match self.0.read_data_store().await {
|
||||
RegistryInit::Ok(data_store) => Ok(Some(Object {
|
||||
inner: data_store.into(),
|
||||
revision: 0,
|
||||
})),
|
||||
RegistryInit::Err(err) => {
|
||||
Err(trc::EventType::Registry(trc::RegistryEvent::LocalReadError)
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.reason(err))
|
||||
}
|
||||
RegistryInit::Bootstrap => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn object<T: ObjectImpl + From<Object>>(&self, id: Id) -> trc::Result<Option<T>> {
|
||||
self.get(ObjectId::new(T::OBJECT, id))
|
||||
.await
|
||||
.map(|v| v.map(T::from))
|
||||
}
|
||||
|
||||
pub async fn list<T: ObjectImpl + From<Object>>(&self) -> trc::Result<Vec<RegistryObject<T>>> {
|
||||
let object_type = T::OBJECT;
|
||||
|
||||
let mut results = Vec::new();
|
||||
self.0
|
||||
.store
|
||||
.iterate(
|
||||
IterateParams::new(
|
||||
ValueKey::from(ValueClass::Any(AnyClass {
|
||||
subspace: SUBSPACE_REGISTRY,
|
||||
key: KeySerializer::new(U16_LEN)
|
||||
.write(object_type.to_id())
|
||||
.finalize(),
|
||||
})),
|
||||
ValueKey::from(ValueClass::Any(AnyClass {
|
||||
subspace: SUBSPACE_REGISTRY,
|
||||
key: KeySerializer::new(U16_LEN + U64_LEN)
|
||||
.write(object_type.to_id())
|
||||
.write(u64::MAX)
|
||||
.finalize(),
|
||||
})),
|
||||
),
|
||||
|key, value| {
|
||||
let id = key.deserialize_be_u64(U16_LEN)?;
|
||||
let object = PickledStream::new(value)
|
||||
.and_then(|mut stream| T::unpickle(&mut stream))
|
||||
.ok_or_else(|| {
|
||||
trc::EventType::Registry(trc::RegistryEvent::DeserializationError)
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.id(id)
|
||||
.details(object_type.as_str())
|
||||
.ctx(trc::Key::Value, value)
|
||||
})?;
|
||||
|
||||
results.push(RegistryObject {
|
||||
id: ObjectId::new(object_type, Id::new(id)),
|
||||
object,
|
||||
revision: xxhash_rust::xxh3::xxh3_64(value),
|
||||
});
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{RegistryStore, RegistryStoreInner, Store};
|
||||
use registry::schema::structs::DataStore;
|
||||
use std::{net::IpAddr, path::PathBuf};
|
||||
use utils::snowflake::SnowflakeIdGenerator;
|
||||
|
||||
pub(crate) enum RegistryInit {
|
||||
Ok(DataStore),
|
||||
Err(String),
|
||||
Bootstrap,
|
||||
}
|
||||
|
||||
impl RegistryStoreInner {
|
||||
pub(crate) fn new(local_path: PathBuf) -> Self {
|
||||
let env_hostname = std::env::var("STALWART_HOSTNAME")
|
||||
.ok()
|
||||
.filter(|h| !h.is_empty())
|
||||
.unwrap_or_else(|| {
|
||||
let host = gethostname::gethostname();
|
||||
let host = host.to_string_lossy();
|
||||
if host.parse::<IpAddr>().is_err() {
|
||||
host.to_lowercase()
|
||||
} else {
|
||||
"localhost".to_string()
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
local_path,
|
||||
store: Store::None,
|
||||
id_generator: SnowflakeIdGenerator::new(),
|
||||
node_id: 0,
|
||||
env_recovery_mode: std::env::var("STALWART_RECOVERY_MODE")
|
||||
.ok()
|
||||
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
||||
.unwrap_or(false),
|
||||
env_recovery_admin: std::env::var("STALWART_RECOVERY_ADMIN")
|
||||
.ok()
|
||||
.and_then(|v| {
|
||||
v.split_once(':')
|
||||
.map(|(a, p)| (a.trim().to_string(), p.trim().to_string()))
|
||||
})
|
||||
.filter(|(a, p)| !a.is_empty() && !p.is_empty()),
|
||||
env_cluster_role: std::env::var("STALWART_ROLE")
|
||||
.ok()
|
||||
.filter(|r| !r.is_empty()),
|
||||
env_push_shard_id: std::env::var("STALWART_PUSH_SHARD")
|
||||
.ok()
|
||||
.and_then(|id| id.parse::<u32>().ok().and_then(|v| v.checked_sub(1)))
|
||||
.unwrap_or(0),
|
||||
env_public_url: std::env::var("STALWART_PUBLIC_URL")
|
||||
.ok()
|
||||
.map(|v| v.trim().trim_end_matches('/').to_string())
|
||||
.filter(|u| !u.is_empty())
|
||||
.or_else(|| {
|
||||
std::env::var("STALWART_HTTPS_PORT").ok().and_then(|p| {
|
||||
p.parse::<u16>()
|
||||
.ok()
|
||||
.map(|port| format!("https://{}:{}", env_hostname, port))
|
||||
})
|
||||
}),
|
||||
env_hostname,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn read_data_store(&self) -> RegistryInit {
|
||||
match tokio::fs::read_to_string(&self.local_path).await {
|
||||
Ok(contents) => match serde_json::from_str::<DataStore>(&contents) {
|
||||
Ok(data_store) => RegistryInit::Ok(data_store),
|
||||
Err(err) => RegistryInit::Err(format!(
|
||||
"Failed to parse data store settings at {}: {}",
|
||||
self.local_path.display(),
|
||||
err
|
||||
)),
|
||||
},
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => RegistryInit::Bootstrap,
|
||||
Err(err) => RegistryInit::Err(format!(
|
||||
"Failed to read data store settings at {}: {}",
|
||||
self.local_path.display(),
|
||||
err
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryStore {
|
||||
pub async fn write_data_store(&self, data_store: &DataStore) -> trc::Result<()> {
|
||||
let json_text = serde_json::to_string(data_store).map_err(|err| {
|
||||
trc::EventType::Registry(trc::RegistryEvent::LocalWriteError)
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.reason(err)
|
||||
})?;
|
||||
tokio::fs::write(&self.0.local_path, json_text)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
trc::EventType::Registry(trc::RegistryEvent::LocalWriteError)
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.reason(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod bootstrap;
|
||||
pub mod get;
|
||||
pub mod local;
|
||||
pub mod query;
|
||||
pub mod write;
|
||||
|
||||
use crate::{
|
||||
Deserialize, SerializeInfallible, U16_LEN, U32_LEN, U64_LEN,
|
||||
write::key::{DeserializeBigEndian, KeySerializer},
|
||||
};
|
||||
use registry::{
|
||||
pickle::{Pickle, PickledStream},
|
||||
schema::{
|
||||
prelude::{Object, ObjectInner, ObjectType, Property},
|
||||
structs::{
|
||||
ArchivedItem, DmarcInternalReport, Metric, SpamTrainingSample, Task, TlsInternalReport,
|
||||
Trace,
|
||||
},
|
||||
},
|
||||
types::{EnumImpl, ObjectImpl, id::ObjectId},
|
||||
};
|
||||
use types::id::Id;
|
||||
|
||||
pub struct RegistryObject<T: ObjectImpl> {
|
||||
pub id: ObjectId,
|
||||
pub object: T,
|
||||
pub revision: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RegistryQuery {
|
||||
pub(crate) object_type: ObjectType,
|
||||
pub filters: Vec<RegistryFilter>,
|
||||
pub(crate) start: RegistryQueryStart,
|
||||
pub(crate) limit: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct RegistryObjectCounter(pub usize);
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) enum RegistryQueryStart {
|
||||
Index(u64),
|
||||
Anchor(u64),
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RegistryFilter {
|
||||
pub property: Property,
|
||||
pub op: RegistryFilterOp,
|
||||
pub value: RegistryFilterValue,
|
||||
pub is_pk: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Eq, Hash)]
|
||||
pub struct ObjectIdVersioned {
|
||||
pub object_id: ObjectId,
|
||||
pub version: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RegistryFilterOp {
|
||||
Equal,
|
||||
GreaterThan,
|
||||
GreaterEqualThan,
|
||||
LowerThan,
|
||||
LowerEqualThan,
|
||||
TextMatch,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RegistryFilterValue {
|
||||
String(String),
|
||||
Bytes(Vec<u8>),
|
||||
U64(u64),
|
||||
U16(u16),
|
||||
Boolean(bool),
|
||||
}
|
||||
|
||||
impl Deserialize for Object {
|
||||
fn deserialize_with_key(key: &[u8], bytes: &[u8]) -> trc::Result<Self> {
|
||||
let revision = xxhash_rust::xxh3::xxh3_64(bytes);
|
||||
ObjectType::from_id(key.deserialize_be_u16(0)?)
|
||||
.and_then(|object_id| ObjectInner::unpickle(object_id, &mut PickledStream::new(bytes)?))
|
||||
.map(|inner| Object { revision, inner })
|
||||
.ok_or_else(|| {
|
||||
trc::EventType::Registry(trc::RegistryEvent::DeserializationError)
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.ctx(trc::Key::Value, bytes)
|
||||
})
|
||||
}
|
||||
|
||||
fn deserialize(_: &[u8]) -> trc::Result<Self> {
|
||||
unreachable!("Object deserialization requires the object type from the key")
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for Task {
|
||||
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
|
||||
PickledStream::new(bytes)
|
||||
.and_then(|mut stream| Self::unpickle(&mut stream))
|
||||
.ok_or_else(|| {
|
||||
trc::EventType::Registry(trc::RegistryEvent::DeserializationError)
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.ctx(trc::Key::Value, bytes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for SpamTrainingSample {
|
||||
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
|
||||
PickledStream::new(bytes)
|
||||
.and_then(|mut stream| Self::unpickle(&mut stream))
|
||||
.ok_or_else(|| {
|
||||
trc::EventType::Registry(trc::RegistryEvent::DeserializationError)
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.ctx(trc::Key::Value, bytes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for ArchivedItem {
|
||||
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
|
||||
PickledStream::new(bytes)
|
||||
.and_then(|mut stream| Self::unpickle(&mut stream))
|
||||
.ok_or_else(|| {
|
||||
trc::EventType::Registry(trc::RegistryEvent::DeserializationError)
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.ctx(trc::Key::Value, bytes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for TlsInternalReport {
|
||||
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
|
||||
PickledStream::new(bytes)
|
||||
.and_then(|mut stream| Self::unpickle(&mut stream))
|
||||
.ok_or_else(|| {
|
||||
trc::EventType::Registry(trc::RegistryEvent::DeserializationError)
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.ctx(trc::Key::Value, bytes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for DmarcInternalReport {
|
||||
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
|
||||
PickledStream::new(bytes)
|
||||
.and_then(|mut stream| Self::unpickle(&mut stream))
|
||||
.ok_or_else(|| {
|
||||
trc::EventType::Registry(trc::RegistryEvent::DeserializationError)
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.ctx(trc::Key::Value, bytes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializeInfallible for ObjectId {
|
||||
fn serialize(&self) -> Vec<u8> {
|
||||
KeySerializer::new(U16_LEN + U64_LEN)
|
||||
.write(self.object().to_id())
|
||||
.write(self.id().id())
|
||||
.finalize()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for ObjectId {
|
||||
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
|
||||
let object_id = bytes.deserialize_be_u16(0)?;
|
||||
let item_id = bytes.deserialize_be_u64(U16_LEN)?;
|
||||
Ok(ObjectId::new(
|
||||
ObjectType::from_id(object_id).ok_or_else(|| {
|
||||
trc::EventType::Registry(trc::RegistryEvent::DeserializationError)
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.ctx(trc::Key::Value, bytes)
|
||||
})?,
|
||||
Id::new(item_id),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializeInfallible for ObjectIdVersioned {
|
||||
fn serialize(&self) -> Vec<u8> {
|
||||
KeySerializer::new(U16_LEN + U64_LEN + U32_LEN)
|
||||
.write(self.object_id.object().to_id())
|
||||
.write(self.object_id.id().id())
|
||||
.write(self.version)
|
||||
.finalize()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for ObjectIdVersioned {
|
||||
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
|
||||
let object_id = ObjectId::deserialize(bytes)?;
|
||||
let version = bytes.deserialize_be_u32(U16_LEN + U64_LEN)?;
|
||||
Ok(Self { object_id, version })
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for Trace {
|
||||
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
|
||||
PickledStream::new(bytes)
|
||||
.and_then(|mut stream| Self::unpickle(&mut stream))
|
||||
.ok_or_else(|| {
|
||||
trc::EventType::Registry(trc::RegistryEvent::DeserializationError)
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.ctx(trc::Key::Value, bytes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for Metric {
|
||||
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
|
||||
PickledStream::new(bytes)
|
||||
.and_then(|mut stream| Self::unpickle(&mut stream))
|
||||
.ok_or_else(|| {
|
||||
trc::EventType::Registry(trc::RegistryEvent::DeserializationError)
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.ctx(trc::Key::Value, bytes)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,943 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
IterateParams, RegistryStore, SUBSPACE_REGISTRY_IDX, SUBSPACE_REGISTRY_PK, Store, U16_LEN,
|
||||
U64_LEN, ValueKey,
|
||||
registry::{
|
||||
RegistryFilter, RegistryFilterOp, RegistryFilterValue, RegistryObjectCounter,
|
||||
RegistryQuery, RegistryQueryStart,
|
||||
},
|
||||
write::{
|
||||
AnyClass, RegistryClass, ValueClass,
|
||||
key::{DeserializeBigEndian, KeySerializer},
|
||||
},
|
||||
};
|
||||
use ahash::AHashSet;
|
||||
use registry::{
|
||||
schema::prelude::{OBJ_FILTER_ACCOUNT, OBJ_FILTER_TENANT, OBJ_SINGLETON, ObjectType, Property},
|
||||
types::{EnumImpl, id::ObjectId},
|
||||
};
|
||||
use roaring::RoaringBitmap;
|
||||
use std::{borrow::Cow, ops::BitAndAssign};
|
||||
use trc::AddContext;
|
||||
use types::id::Id;
|
||||
|
||||
impl RegistryStore {
|
||||
pub async fn query<T: RegistryQueryResults>(&self, query: RegistryQuery) -> trc::Result<T> {
|
||||
if query.filters.is_empty() {
|
||||
return all_ids::<T>(&self.0.store, query).await;
|
||||
}
|
||||
|
||||
let mut u64_buffer;
|
||||
let mut u16_buffer;
|
||||
let mut bool_buffer = [0u8; 1];
|
||||
|
||||
let mut results = ResultsPagination::<T>::new(&query);
|
||||
for filter in &query.filters {
|
||||
if filter.op == RegistryFilterOp::TextMatch {
|
||||
if let RegistryFilterValue::String(text) = &filter.value {
|
||||
let mut matches = ResultsPagination::<T>::new(&query);
|
||||
|
||||
for word in text
|
||||
.split(|c: char| !c.is_alphanumeric())
|
||||
.filter(|s| s.len() > 1)
|
||||
{
|
||||
let word = if word
|
||||
.chars()
|
||||
.all(|ch| ch.is_lowercase() || !ch.is_alphabetic())
|
||||
{
|
||||
Cow::Borrowed(word)
|
||||
} else {
|
||||
Cow::Owned(word.to_lowercase())
|
||||
};
|
||||
|
||||
let mut result = ResultsPagination::<T>::new(&query);
|
||||
|
||||
index_range(
|
||||
&self.0.store,
|
||||
query.object_type,
|
||||
filter.property.to_id(),
|
||||
word.as_bytes(),
|
||||
RegistryFilterOp::Equal,
|
||||
&mut result,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !matches.list.has_items() {
|
||||
matches = result;
|
||||
} else {
|
||||
matches.list.intersect(&result.list);
|
||||
if !matches.list.has_items() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !results.list.has_items() {
|
||||
results = matches;
|
||||
} else {
|
||||
results.list.intersect(&matches.list);
|
||||
}
|
||||
} else {
|
||||
return Err(trc::EventType::Registry(trc::RegistryEvent::NotSupported)
|
||||
.into_err()
|
||||
.details("TextMatch operator only supports string values"));
|
||||
}
|
||||
} else {
|
||||
let value = match &filter.value {
|
||||
RegistryFilterValue::String(v) => v.as_bytes(),
|
||||
RegistryFilterValue::Bytes(v) => v.as_slice(),
|
||||
RegistryFilterValue::U64(v) => {
|
||||
u64_buffer = v.to_be_bytes();
|
||||
&u64_buffer
|
||||
}
|
||||
RegistryFilterValue::U16(v) => {
|
||||
u16_buffer = v.to_be_bytes();
|
||||
&u16_buffer
|
||||
}
|
||||
RegistryFilterValue::Boolean(v) => {
|
||||
bool_buffer[0] = *v as u8;
|
||||
&bool_buffer
|
||||
}
|
||||
};
|
||||
|
||||
let mut result = ResultsPagination::<T>::new(&query);
|
||||
if !filter.is_pk {
|
||||
index_range(
|
||||
&self.0.store,
|
||||
query.object_type,
|
||||
filter.property.to_id(),
|
||||
value,
|
||||
filter.op,
|
||||
&mut result,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
pk_range(
|
||||
&self.0.store,
|
||||
query.object_type,
|
||||
filter.property.to_id(),
|
||||
value,
|
||||
filter.op,
|
||||
&mut result,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
if !results.list.has_items() {
|
||||
results = result;
|
||||
} else {
|
||||
results.list.intersect(&result.list);
|
||||
}
|
||||
}
|
||||
|
||||
if !results.list.has_items() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results.finalize())
|
||||
}
|
||||
|
||||
pub async fn count_object(&self, object_type: ObjectType) -> trc::Result<usize> {
|
||||
if object_type.flags() & OBJ_SINGLETON == 0 {
|
||||
self.query::<RegistryObjectCounter>(RegistryQuery::new(object_type))
|
||||
.await
|
||||
.map(|r| r.0)
|
||||
} else {
|
||||
self.store()
|
||||
.key_exists(ValueKey::from(RegistryClass::Item {
|
||||
object_id: object_type.to_id(),
|
||||
item_id: Id::singleton().id(),
|
||||
}))
|
||||
.await
|
||||
.caused_by(trc::location!())
|
||||
.map(|exists| if exists { 1 } else { 0 })
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn primary_key(
|
||||
&self,
|
||||
object_type: Option<ObjectType>,
|
||||
property: Property,
|
||||
key: Vec<u8>,
|
||||
) -> trc::Result<Option<ObjectId>> {
|
||||
self.store()
|
||||
.get_value::<ObjectId>(ValueKey::from(ValueClass::Registry(
|
||||
RegistryClass::PrimaryKey {
|
||||
object_id: object_type.map(|obj| obj.to_id()),
|
||||
index_id: property.to_id(),
|
||||
key,
|
||||
},
|
||||
)))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn sort_by_index(
|
||||
&self,
|
||||
object: ObjectType,
|
||||
property: Property,
|
||||
ids: Option<Vec<Id>>,
|
||||
ascending: bool,
|
||||
) -> trc::Result<Vec<Id>> {
|
||||
let mut ids = ids.map(|ids| ids.into_iter().collect::<AHashSet<_>>());
|
||||
let mut ids_sorted = Vec::with_capacity(ids.as_ref().map_or(0, |ids| ids.len()));
|
||||
|
||||
let object_id = object.to_id();
|
||||
let index_id = property.to_id();
|
||||
let begin = ValueKey::from(ValueClass::Any(AnyClass {
|
||||
subspace: SUBSPACE_REGISTRY_IDX,
|
||||
key: KeySerializer::new(U16_LEN * 2)
|
||||
.write(object_id)
|
||||
.write(index_id)
|
||||
.finalize(),
|
||||
}));
|
||||
let end = ValueKey::from(ValueClass::Any(AnyClass {
|
||||
subspace: SUBSPACE_REGISTRY_IDX,
|
||||
key: KeySerializer::new((U16_LEN * 2) + U64_LEN)
|
||||
.write(object_id)
|
||||
.write(index_id)
|
||||
.write(u64::MAX)
|
||||
.finalize(),
|
||||
}));
|
||||
|
||||
self.0
|
||||
.store
|
||||
.iterate(
|
||||
IterateParams::new(begin, end)
|
||||
.no_values()
|
||||
.set_ascending(ascending),
|
||||
|key, _| {
|
||||
let id = Id::from(key.deserialize_be_u64(key.len() - U64_LEN)?);
|
||||
if let Some(ids) = ids.as_mut() {
|
||||
if ids.remove(&id) {
|
||||
ids_sorted.push(id);
|
||||
}
|
||||
Ok(!ids.is_empty())
|
||||
} else {
|
||||
ids_sorted.push(id);
|
||||
Ok(true)
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())
|
||||
.map(|_| {
|
||||
if let Some(mut ids) = ids
|
||||
&& !ids.is_empty()
|
||||
{
|
||||
ids_sorted.extend(ids.drain());
|
||||
}
|
||||
|
||||
ids_sorted
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn sort_by_pk(
|
||||
&self,
|
||||
object: ObjectType,
|
||||
property: Property,
|
||||
ids: Option<Vec<Id>>,
|
||||
ascending: bool,
|
||||
) -> trc::Result<Vec<Id>> {
|
||||
let mut ids = ids.map(|ids| ids.into_iter().collect::<AHashSet<_>>());
|
||||
let mut ids_sorted = Vec::with_capacity(ids.as_ref().map_or(0, |ids| ids.len()));
|
||||
|
||||
let object_id = object.to_id();
|
||||
let index_id = property.to_id();
|
||||
let begin = ValueKey::from(ValueClass::Any(AnyClass {
|
||||
subspace: SUBSPACE_REGISTRY_PK,
|
||||
key: KeySerializer::new(U16_LEN * 2)
|
||||
.write(object_id)
|
||||
.write(index_id)
|
||||
.finalize(),
|
||||
}));
|
||||
let end = ValueKey::from(ValueClass::Any(AnyClass {
|
||||
subspace: SUBSPACE_REGISTRY_PK,
|
||||
key: KeySerializer::new((U16_LEN * 2) + U64_LEN)
|
||||
.write(object_id)
|
||||
.write(index_id)
|
||||
.write(u64::MAX)
|
||||
.finalize(),
|
||||
}));
|
||||
|
||||
self.0
|
||||
.store
|
||||
.iterate(
|
||||
IterateParams::new(begin, end).set_ascending(ascending),
|
||||
|_, value| {
|
||||
let id = Id::from(value.deserialize_be_u64(U16_LEN)?);
|
||||
|
||||
if let Some(ids) = ids.as_mut() {
|
||||
if ids.remove(&id) {
|
||||
ids_sorted.push(id);
|
||||
}
|
||||
Ok(!ids.is_empty())
|
||||
} else {
|
||||
ids_sorted.push(id);
|
||||
Ok(true)
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())
|
||||
.map(|_| {
|
||||
if let Some(mut ids) = ids
|
||||
&& !ids.is_empty()
|
||||
{
|
||||
ids_sorted.extend(ids.drain());
|
||||
}
|
||||
|
||||
ids_sorted
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn all_ids<T: RegistryQueryResults>(store: &Store, query: RegistryQuery) -> trc::Result<T> {
|
||||
let mut bm = T::default();
|
||||
let object_id = query.object_type.to_id();
|
||||
|
||||
let (item_id, mut offset) = match query.start {
|
||||
RegistryQueryStart::Index(index) => (0, index),
|
||||
RegistryQueryStart::Anchor(anchor) => (anchor + 1, 0),
|
||||
RegistryQueryStart::None => (0, 0),
|
||||
};
|
||||
|
||||
store
|
||||
.iterate(
|
||||
IterateParams::new(
|
||||
ValueKey::from(ValueClass::Registry(RegistryClass::IndexId {
|
||||
object_id,
|
||||
item_id,
|
||||
})),
|
||||
ValueKey::from(ValueClass::Registry(RegistryClass::IndexId {
|
||||
object_id,
|
||||
item_id: u64::MAX,
|
||||
})),
|
||||
)
|
||||
.no_values()
|
||||
.ascending(),
|
||||
|key, _| {
|
||||
if offset == 0 {
|
||||
bm.push(key.deserialize_be_u64(U16_LEN * 2)?);
|
||||
Ok(query.limit.is_none_or(|limit| bm.count() < limit))
|
||||
} else {
|
||||
offset -= 1;
|
||||
Ok(true)
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())
|
||||
.map(|_| bm)
|
||||
}
|
||||
|
||||
async fn index_range<T: RegistryQueryResults>(
|
||||
store: &Store,
|
||||
object: ObjectType,
|
||||
index_id: u16,
|
||||
match_value: &[u8],
|
||||
op: RegistryFilterOp,
|
||||
results: &mut ResultsPagination<T>,
|
||||
) -> trc::Result<()> {
|
||||
let ((from_value, from_doc_id, from_index_id), (end_value, end_doc_id, end_index_id)) = match op
|
||||
{
|
||||
RegistryFilterOp::LowerThan => ((&[][..], 0, index_id), (match_value, 0, index_id)),
|
||||
RegistryFilterOp::LowerEqualThan => {
|
||||
((&[][..], 0, index_id), (match_value, u64::MAX, index_id))
|
||||
}
|
||||
RegistryFilterOp::GreaterThan => (
|
||||
(match_value, u64::MAX, index_id),
|
||||
(&[][..], u64::MAX, index_id + 1),
|
||||
),
|
||||
RegistryFilterOp::GreaterEqualThan => (
|
||||
(match_value, 0, index_id),
|
||||
(&[][..], u64::MAX, index_id + 1),
|
||||
),
|
||||
RegistryFilterOp::Equal | RegistryFilterOp::TextMatch => (
|
||||
(match_value, 0, index_id),
|
||||
(match_value, u64::MAX, index_id),
|
||||
),
|
||||
};
|
||||
|
||||
let object_id = object.to_id();
|
||||
let begin = ValueKey::from(ValueClass::Any(AnyClass {
|
||||
subspace: SUBSPACE_REGISTRY_IDX,
|
||||
key: KeySerializer::new((U16_LEN * 2) + U64_LEN + from_value.len())
|
||||
.write(object_id)
|
||||
.write(from_index_id)
|
||||
.write(from_value)
|
||||
.write(from_doc_id)
|
||||
.finalize(),
|
||||
}));
|
||||
let end = ValueKey::from(ValueClass::Any(AnyClass {
|
||||
subspace: SUBSPACE_REGISTRY_IDX,
|
||||
key: KeySerializer::new((U16_LEN * 2) + U64_LEN + end_value.len())
|
||||
.write(object_id)
|
||||
.write(end_index_id)
|
||||
.write(end_value)
|
||||
.write(end_doc_id)
|
||||
.finalize(),
|
||||
}));
|
||||
|
||||
let prefix = KeySerializer::new(U16_LEN * 2)
|
||||
.write(object_id)
|
||||
.write(index_id)
|
||||
.finalize();
|
||||
|
||||
store
|
||||
.iterate(
|
||||
IterateParams::new(begin, end).no_values().ascending(),
|
||||
|key, _| {
|
||||
if !key.starts_with(&prefix) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let id_pos = key.len() - U64_LEN;
|
||||
let value = key
|
||||
.get(U16_LEN * 2..id_pos)
|
||||
.ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?;
|
||||
|
||||
let matches = match op {
|
||||
RegistryFilterOp::LowerThan => value < match_value,
|
||||
RegistryFilterOp::LowerEqualThan => value <= match_value,
|
||||
RegistryFilterOp::GreaterThan => value > match_value,
|
||||
RegistryFilterOp::GreaterEqualThan => value >= match_value,
|
||||
RegistryFilterOp::Equal | RegistryFilterOp::TextMatch => value == match_value,
|
||||
};
|
||||
|
||||
if matches {
|
||||
Ok(results.push(key.deserialize_be_u64(id_pos)?))
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())
|
||||
.inspect(|_| results.list.sort())
|
||||
}
|
||||
|
||||
async fn pk_range<T: RegistryQueryResults>(
|
||||
store: &Store,
|
||||
object: ObjectType,
|
||||
index_id: u16,
|
||||
match_value: &[u8],
|
||||
op: RegistryFilterOp,
|
||||
results: &mut ResultsPagination<T>,
|
||||
) -> trc::Result<()> {
|
||||
let ((from_value, from_index_id), (end_value, end_index_id)) = match op {
|
||||
RegistryFilterOp::LowerThan => ((&[][..], index_id), (match_value, index_id)),
|
||||
RegistryFilterOp::LowerEqualThan => ((&[][..], index_id), (match_value, index_id)),
|
||||
RegistryFilterOp::GreaterThan => ((match_value, index_id), (&[][..], index_id + 1)),
|
||||
RegistryFilterOp::GreaterEqualThan => ((match_value, index_id), (&[][..], index_id + 1)),
|
||||
RegistryFilterOp::Equal | RegistryFilterOp::TextMatch => {
|
||||
((match_value, index_id), (match_value, index_id))
|
||||
}
|
||||
};
|
||||
|
||||
let object_id = object.to_id();
|
||||
let begin = ValueKey::from(ValueClass::Any(AnyClass {
|
||||
subspace: SUBSPACE_REGISTRY_PK,
|
||||
key: KeySerializer::new((U16_LEN * 2) + from_value.len())
|
||||
.write(object_id)
|
||||
.write(from_index_id)
|
||||
.write(from_value)
|
||||
.finalize(),
|
||||
}));
|
||||
let end = ValueKey::from(ValueClass::Any(AnyClass {
|
||||
subspace: SUBSPACE_REGISTRY_PK,
|
||||
key: KeySerializer::new((U16_LEN * 2) + end_value.len())
|
||||
.write(object_id)
|
||||
.write(end_index_id)
|
||||
.write(end_value)
|
||||
.finalize(),
|
||||
}));
|
||||
|
||||
let prefix = KeySerializer::new(U16_LEN * 2)
|
||||
.write(object_id)
|
||||
.write(index_id)
|
||||
.finalize();
|
||||
|
||||
store
|
||||
.iterate(IterateParams::new(begin, end).ascending(), |key, value| {
|
||||
if !key.starts_with(&prefix) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let key = key
|
||||
.get(U16_LEN * 2..)
|
||||
.ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?;
|
||||
|
||||
let matches = match op {
|
||||
RegistryFilterOp::LowerThan => key < match_value,
|
||||
RegistryFilterOp::LowerEqualThan => key <= match_value,
|
||||
RegistryFilterOp::GreaterThan => key > match_value,
|
||||
RegistryFilterOp::GreaterEqualThan => key >= match_value,
|
||||
RegistryFilterOp::Equal | RegistryFilterOp::TextMatch => key == match_value,
|
||||
};
|
||||
|
||||
if matches {
|
||||
Ok(results.push(value.deserialize_be_u64(U16_LEN)?))
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
})
|
||||
.await
|
||||
.caused_by(trc::location!())
|
||||
.inspect(|_| results.list.sort())
|
||||
}
|
||||
|
||||
pub trait RegistryQueryResults: Default + Sized + Sync + Send {
|
||||
fn push(&mut self, id: u64);
|
||||
fn has_items(&self) -> bool;
|
||||
fn intersect(&mut self, other: &Self);
|
||||
fn count(&self) -> usize;
|
||||
fn sort(&mut self);
|
||||
fn into_list(self) -> impl Iterator<Item = u64>;
|
||||
}
|
||||
|
||||
impl RegistryQueryResults for Vec<Id> {
|
||||
fn push(&mut self, id: u64) {
|
||||
self.push(Id::new(id));
|
||||
}
|
||||
|
||||
fn has_items(&self) -> bool {
|
||||
!self.is_empty()
|
||||
}
|
||||
|
||||
fn intersect(&mut self, other: &Self) {
|
||||
let a = self;
|
||||
let b = other;
|
||||
let mut i = 0;
|
||||
let mut j = 0;
|
||||
let mut write = 0;
|
||||
|
||||
while i < a.len() && j < b.len() {
|
||||
if a[i] < b[j] {
|
||||
let target = b[j];
|
||||
let remain = &a[i..];
|
||||
i += remain.partition_point(|&x| x < target);
|
||||
} else if a[i] > b[j] {
|
||||
let target = a[i];
|
||||
let remain = &b[j..];
|
||||
j += remain.partition_point(|&x| x < target);
|
||||
} else {
|
||||
a[write] = a[i];
|
||||
write += 1;
|
||||
i += 1;
|
||||
j += 1;
|
||||
}
|
||||
}
|
||||
a.truncate(write);
|
||||
}
|
||||
|
||||
fn count(&self) -> usize {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn sort(&mut self) {
|
||||
match self.len() {
|
||||
0 | 1 => {}
|
||||
..3000 => self.sort_unstable(),
|
||||
_ => radsort::sort_by_key(self, |id| id.id()),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_list(self) -> impl Iterator<Item = u64> {
|
||||
self.into_iter().map(|id| id.id())
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryQueryResults for RoaringBitmap {
|
||||
fn push(&mut self, id: u64) {
|
||||
self.insert(id as u32);
|
||||
}
|
||||
|
||||
fn has_items(&self) -> bool {
|
||||
!self.is_empty()
|
||||
}
|
||||
|
||||
fn intersect(&mut self, other: &Self) {
|
||||
self.bitand_assign(other);
|
||||
}
|
||||
|
||||
fn count(&self) -> usize {
|
||||
self.len() as usize
|
||||
}
|
||||
|
||||
fn sort(&mut self) {}
|
||||
|
||||
fn into_list(self) -> impl Iterator<Item = u64> {
|
||||
self.into_iter().map(|id| id as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryQueryResults for RegistryObjectCounter {
|
||||
fn push(&mut self, _: u64) {
|
||||
self.0 += 1;
|
||||
}
|
||||
|
||||
fn has_items(&self) -> bool {
|
||||
self.0 > 0
|
||||
}
|
||||
|
||||
fn intersect(&mut self, _: &Self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn count(&self) -> usize {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn sort(&mut self) {}
|
||||
|
||||
fn into_list(self) -> impl Iterator<Item = u64> {
|
||||
Vec::new().into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
struct ResultsPagination<T: RegistryQueryResults> {
|
||||
list: T,
|
||||
offset: usize,
|
||||
anchor: Option<u64>,
|
||||
limit: Option<usize>,
|
||||
deferred_pagination: bool,
|
||||
}
|
||||
|
||||
impl<T: RegistryQueryResults> ResultsPagination<T> {
|
||||
fn new(query: &RegistryQuery) -> Self {
|
||||
let (anchor, offset) = match query.start {
|
||||
RegistryQueryStart::Index(index) => (None, index),
|
||||
RegistryQueryStart::Anchor(anchor) => (Some(anchor), 0),
|
||||
RegistryQueryStart::None => (None, 0),
|
||||
};
|
||||
|
||||
Self {
|
||||
list: T::default(),
|
||||
offset: offset as usize,
|
||||
anchor,
|
||||
limit: query.limit,
|
||||
deferred_pagination: query.filters.len() > 1
|
||||
|| query.filters.first().is_some_and(|f| {
|
||||
if let (RegistryFilterOp::TextMatch, RegistryFilterValue::String(value)) =
|
||||
(&f.op, &f.value)
|
||||
{
|
||||
value.chars().any(|c| !c.is_alphanumeric()) && value.len() > 1
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, id: u64) -> bool {
|
||||
if !self.deferred_pagination {
|
||||
if self.offset > 0 {
|
||||
self.offset -= 1;
|
||||
true
|
||||
} else if let Some(anchor) = self.anchor {
|
||||
if id == anchor {
|
||||
self.anchor = None;
|
||||
}
|
||||
true
|
||||
} else {
|
||||
self.list.push(id);
|
||||
self.limit.is_none_or(|limit| self.list.count() < limit)
|
||||
}
|
||||
} else {
|
||||
self.list.push(id);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize(mut self) -> T {
|
||||
if self.deferred_pagination
|
||||
&& self.list.has_items()
|
||||
&& (self.limit.is_some() || self.anchor.is_some() || self.offset > 0)
|
||||
{
|
||||
let list = std::mem::take(&mut self.list);
|
||||
self.deferred_pagination = false;
|
||||
|
||||
for item in list.into_list() {
|
||||
if !self.push(item) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.list
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryQuery {
|
||||
pub fn new(object_type: ObjectType) -> Self {
|
||||
Self {
|
||||
object_type,
|
||||
filters: Vec::new(),
|
||||
start: RegistryQueryStart::None,
|
||||
limit: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_anchor(mut self, anchor: u64) -> Self {
|
||||
self.start = RegistryQueryStart::Anchor(anchor);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_index_start(mut self, index: u64) -> Self {
|
||||
self.start = RegistryQueryStart::Index(index);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_limit(mut self, limit: usize) -> Self {
|
||||
self.limit = Some(limit);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_account(mut self, account_id: u32) -> Self {
|
||||
if self.object_type.flags() & OBJ_FILTER_ACCOUNT != 0 {
|
||||
let filter = RegistryFilter::equal(Property::AccountId, account_id, false);
|
||||
if self.filters.is_empty() {
|
||||
self.filters.push(filter);
|
||||
} else {
|
||||
self.filters.insert(0, filter);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_account_opt(self, account_id: Option<u32>) -> Self {
|
||||
if let Some(account_id) = account_id {
|
||||
self.with_account(account_id)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_tenant(mut self, tenant_id: Option<u32>) -> Self {
|
||||
if let Some(tenant_id) = tenant_id
|
||||
&& self.object_type.flags() & OBJ_FILTER_TENANT != 0
|
||||
{
|
||||
let filter = RegistryFilter::equal(Property::MemberTenantId, tenant_id, false);
|
||||
if self.filters.is_empty() {
|
||||
self.filters.push(filter);
|
||||
} else {
|
||||
self.filters.insert(0, filter);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn filter(mut self, filter: RegistryFilter) -> Self {
|
||||
self.filters.push(filter);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn equal(mut self, property: Property, value: impl Into<RegistryFilterValue>) -> Self {
|
||||
self.filters
|
||||
.push(RegistryFilter::equal(property, value, false));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn equal_pk(
|
||||
mut self,
|
||||
property: Property,
|
||||
value: impl Into<RegistryFilterValue>,
|
||||
is_pk: bool,
|
||||
) -> Self {
|
||||
self.filters
|
||||
.push(RegistryFilter::equal(property, value, is_pk));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn push_equal_pk(
|
||||
&mut self,
|
||||
property: Property,
|
||||
value: impl Into<RegistryFilterValue>,
|
||||
is_pk: bool,
|
||||
) {
|
||||
self.filters
|
||||
.push(RegistryFilter::equal(property, value, is_pk));
|
||||
}
|
||||
|
||||
pub fn equal_opt(
|
||||
mut self,
|
||||
property: Property,
|
||||
value: Option<impl Into<RegistryFilterValue>>,
|
||||
) -> Self {
|
||||
if let Some(value) = value {
|
||||
self.filters
|
||||
.push(RegistryFilter::equal(property, value, false));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn greater_than(
|
||||
mut self,
|
||||
property: Property,
|
||||
value: impl Into<RegistryFilterValue>,
|
||||
) -> Self {
|
||||
self.filters
|
||||
.push(RegistryFilter::greater_than(property, value, false));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn less_than(mut self, property: Property, value: impl Into<RegistryFilterValue>) -> Self {
|
||||
self.filters
|
||||
.push(RegistryFilter::less_than(property, value, false));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn greater_than_or_equal(
|
||||
mut self,
|
||||
property: Property,
|
||||
value: impl Into<RegistryFilterValue>,
|
||||
) -> Self {
|
||||
self.filters.push(RegistryFilter::greater_than_or_equal(
|
||||
property, value, false,
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn less_than_or_equal(
|
||||
mut self,
|
||||
property: Property,
|
||||
value: impl Into<RegistryFilterValue>,
|
||||
) -> Self {
|
||||
self.filters
|
||||
.push(RegistryFilter::less_than_or_equal(property, value, false));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn text(mut self, property: Property, value: impl Into<String>) -> Self {
|
||||
self.filters.push(RegistryFilter::text(property, value));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn text_opt(mut self, property: Property, value: Option<impl Into<String>>) -> Self {
|
||||
if let Some(value) = value {
|
||||
self.filters.push(RegistryFilter::text(property, value));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn push_text(&mut self, property: Property, value: impl Into<String>) {
|
||||
self.filters.push(RegistryFilter::text(property, value));
|
||||
}
|
||||
|
||||
pub fn has_filters(&self) -> bool {
|
||||
!self.filters.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryFilter {
|
||||
pub fn text(property: Property, value: impl Into<String>) -> Self {
|
||||
Self {
|
||||
property,
|
||||
op: RegistryFilterOp::TextMatch,
|
||||
value: RegistryFilterValue::String(value.into()),
|
||||
is_pk: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn equal(property: Property, value: impl Into<RegistryFilterValue>, is_pk: bool) -> Self {
|
||||
Self {
|
||||
property,
|
||||
op: RegistryFilterOp::Equal,
|
||||
value: value.into(),
|
||||
is_pk,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn greater_than(
|
||||
property: Property,
|
||||
value: impl Into<RegistryFilterValue>,
|
||||
is_pk: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
property,
|
||||
op: RegistryFilterOp::GreaterThan,
|
||||
value: value.into(),
|
||||
is_pk,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn less_than(
|
||||
property: Property,
|
||||
value: impl Into<RegistryFilterValue>,
|
||||
is_pk: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
property,
|
||||
op: RegistryFilterOp::LowerThan,
|
||||
value: value.into(),
|
||||
is_pk,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn greater_than_or_equal(
|
||||
property: Property,
|
||||
value: impl Into<RegistryFilterValue>,
|
||||
is_pk: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
property,
|
||||
op: RegistryFilterOp::GreaterEqualThan,
|
||||
value: value.into(),
|
||||
is_pk,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn less_than_or_equal(
|
||||
property: Property,
|
||||
value: impl Into<RegistryFilterValue>,
|
||||
is_pk: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
property,
|
||||
op: RegistryFilterOp::LowerEqualThan,
|
||||
value: value.into(),
|
||||
is_pk,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for RegistryFilterValue {
|
||||
fn from(value: String) -> Self {
|
||||
RegistryFilterValue::String(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for RegistryFilterValue {
|
||||
fn from(value: &str) -> Self {
|
||||
RegistryFilterValue::String(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for RegistryFilterValue {
|
||||
fn from(value: u64) -> Self {
|
||||
RegistryFilterValue::U64(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for RegistryFilterValue {
|
||||
fn from(value: u32) -> Self {
|
||||
RegistryFilterValue::U64(value as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for RegistryFilterValue {
|
||||
fn from(value: u16) -> Self {
|
||||
RegistryFilterValue::U16(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for RegistryFilterValue {
|
||||
fn from(value: bool) -> Self {
|
||||
RegistryFilterValue::Boolean(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,632 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
IterateParams, RegistryStore, SerializeInfallible, U16_LEN, U64_LEN, ValueKey,
|
||||
write::{
|
||||
BatchBuilder, RegistryClass, ValueClass,
|
||||
assert::AssertValue,
|
||||
key::{DeserializeBigEndian, KeySerializer},
|
||||
},
|
||||
};
|
||||
use registry::{
|
||||
schema::prelude::{
|
||||
OBJ_FILTER_ACCOUNT, OBJ_FILTER_TENANT, OBJ_SEQ_ID, OBJ_SINGLETON, Object, ObjectInner,
|
||||
ObjectType, Property,
|
||||
},
|
||||
types::{
|
||||
EnumImpl,
|
||||
error::ValidationError,
|
||||
id::ObjectId,
|
||||
index::{IndexBuilder, IndexKey, IndexValue},
|
||||
},
|
||||
};
|
||||
use std::{borrow::Cow, fmt::Display};
|
||||
use trc::AddContext;
|
||||
use types::id::Id;
|
||||
|
||||
const MAX_OBJECT_PAYLOAD_SIZE: usize = 200_000;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum RegistryWriteResult {
|
||||
Success(Id),
|
||||
CannotDeleteLinked {
|
||||
object_id: ObjectId,
|
||||
linked_objects: Vec<ObjectId>,
|
||||
},
|
||||
InvalidSingletonId,
|
||||
CannotDeleteSingleton,
|
||||
NotFound {
|
||||
object_id: ObjectId,
|
||||
},
|
||||
InvalidForeignKey {
|
||||
object_id: ObjectId,
|
||||
},
|
||||
PrimaryKeyConflict {
|
||||
property: Property,
|
||||
existing_id: ObjectId,
|
||||
},
|
||||
ValidationError {
|
||||
errors: Vec<ValidationError>,
|
||||
},
|
||||
NotSupported,
|
||||
}
|
||||
|
||||
pub enum RegistryWrite<'x> {
|
||||
Insert {
|
||||
object: &'x Object,
|
||||
id: Option<Id>,
|
||||
},
|
||||
Update {
|
||||
object: &'x Object,
|
||||
id: Id,
|
||||
old_object: &'x Object,
|
||||
},
|
||||
Delete {
|
||||
object_id: ObjectId,
|
||||
object: Option<&'x Object>,
|
||||
allowed_orphan_types: &'x [ObjectType],
|
||||
},
|
||||
}
|
||||
|
||||
impl RegistryStore {
|
||||
pub async fn write(&self, write: RegistryWrite<'_>) -> trc::Result<RegistryWriteResult> {
|
||||
let mut set_index = IndexBuilder::default();
|
||||
let mut clear_index = IndexBuilder::default();
|
||||
|
||||
let object;
|
||||
let object_type;
|
||||
let object_flags;
|
||||
let object_id;
|
||||
let mut item_id;
|
||||
|
||||
let mut batch = BatchBuilder::new();
|
||||
let mut write_id = true;
|
||||
let mut generate_id = false;
|
||||
|
||||
match write {
|
||||
RegistryWrite::Insert {
|
||||
object: insert_object,
|
||||
id,
|
||||
} => {
|
||||
object = insert_object;
|
||||
object_flags = object.flags();
|
||||
object_type = object.object_type();
|
||||
object_id = object_type.to_id();
|
||||
object.index(&mut set_index);
|
||||
|
||||
item_id = if let Some(id) = id {
|
||||
id.id()
|
||||
} else if object_flags & OBJ_SINGLETON != 0 {
|
||||
write_id = false;
|
||||
Id::singleton().id()
|
||||
} else if object_flags & OBJ_SEQ_ID != 0 {
|
||||
generate_id = true;
|
||||
u64::MAX
|
||||
} else {
|
||||
self.0.id_generator.generate()
|
||||
};
|
||||
}
|
||||
RegistryWrite::Update {
|
||||
object: update_object,
|
||||
id,
|
||||
old_object,
|
||||
} => {
|
||||
object = update_object;
|
||||
object_flags = object.flags();
|
||||
object_type = object.object_type();
|
||||
object_id = object_type.to_id();
|
||||
object.index(&mut set_index);
|
||||
|
||||
// Obtain changes
|
||||
let mut old_index = IndexBuilder::default();
|
||||
old_object.index(&mut old_index);
|
||||
for key in &old_index.keys {
|
||||
if !set_index.keys.contains(key) {
|
||||
clear_index.keys.insert(key.clone());
|
||||
}
|
||||
}
|
||||
set_index.keys.retain(|key| !old_index.keys.contains(key));
|
||||
|
||||
// Validate singleton
|
||||
if object_flags & OBJ_SINGLETON != 0 && !id.is_singleton() {
|
||||
return Ok(RegistryWriteResult::InvalidSingletonId);
|
||||
}
|
||||
|
||||
// Assert value
|
||||
item_id = id.id();
|
||||
batch.assert_value(
|
||||
ValueClass::Registry(RegistryClass::Item { object_id, item_id }),
|
||||
AssertValue::Hash(old_object.revision),
|
||||
);
|
||||
}
|
||||
RegistryWrite::Delete {
|
||||
object_id,
|
||||
object,
|
||||
allowed_orphan_types,
|
||||
} => {
|
||||
return if object_id.object().flags() & OBJ_SINGLETON == 0 {
|
||||
self.delete(object_id, object, allowed_orphan_types).await
|
||||
} else {
|
||||
Ok(RegistryWriteResult::CannotDeleteSingleton)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Validate object
|
||||
let mut errors = Vec::new();
|
||||
object.validate(&mut errors);
|
||||
if !errors.is_empty() {
|
||||
return Ok(RegistryWriteResult::ValidationError { errors });
|
||||
}
|
||||
|
||||
// Write to local registry
|
||||
if let ObjectInner::DataStore(data_store) = &object.inner {
|
||||
if generate_id {
|
||||
return Ok(RegistryWriteResult::NotSupported);
|
||||
}
|
||||
|
||||
return self
|
||||
.write_data_store(data_store)
|
||||
.await
|
||||
.map(|_| RegistryWriteResult::Success(Id::singleton()));
|
||||
}
|
||||
|
||||
// Validate foreign keys
|
||||
let tenant_id = object.inner.member_tenant_id().map(|id| id.id());
|
||||
let account_id = object
|
||||
.inner
|
||||
.account_id()
|
||||
.map(|id| id.id())
|
||||
.or_else(|| (object_type == ObjectType::Account).then_some(item_id));
|
||||
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
let set_keys = &set_index.keys;
|
||||
|
||||
#[cfg(feature = "test_mode")]
|
||||
let set_keys = set_index
|
||||
.keys
|
||||
.iter()
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
|
||||
for key in set_keys {
|
||||
match key {
|
||||
IndexKey::ForeignKey {
|
||||
object_id: foreign_id,
|
||||
type_filter,
|
||||
} => {
|
||||
// Verify that the referenced object exists
|
||||
let item_id = foreign_id.id().id();
|
||||
let object_id = foreign_id.object().to_id();
|
||||
let object_flags = foreign_id.object().flags();
|
||||
let key = if type_filter != &IndexValue::None {
|
||||
RegistryClass::Index {
|
||||
index_id: Property::Type.to_id(),
|
||||
object_id,
|
||||
item_id,
|
||||
key: type_filter.serialize(),
|
||||
}
|
||||
} else {
|
||||
RegistryClass::IndexId { object_id, item_id }
|
||||
};
|
||||
|
||||
if !self
|
||||
.0
|
||||
.store
|
||||
.key_exists(ValueKey::from(ValueClass::Registry(key)))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
return Ok(RegistryWriteResult::InvalidForeignKey {
|
||||
object_id: *foreign_id,
|
||||
});
|
||||
} else if let Some(tenant_id) = tenant_id
|
||||
&& (object_flags & OBJ_FILTER_TENANT) != 0
|
||||
&& !self
|
||||
.0
|
||||
.store
|
||||
.key_exists(ValueKey::from(ValueClass::Registry(
|
||||
RegistryClass::Index {
|
||||
index_id: Property::MemberTenantId.to_id(),
|
||||
object_id,
|
||||
item_id,
|
||||
key: IndexValue::U64(tenant_id).serialize(),
|
||||
},
|
||||
)))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
return Ok(RegistryWriteResult::InvalidForeignKey {
|
||||
object_id: *foreign_id,
|
||||
});
|
||||
} else if (object_flags & OBJ_FILTER_ACCOUNT) != 0
|
||||
&& let Some(account_id) = account_id
|
||||
&& !self
|
||||
.0
|
||||
.store
|
||||
.key_exists(ValueKey::from(ValueClass::Registry(
|
||||
RegistryClass::Index {
|
||||
index_id: Property::AccountId.to_id(),
|
||||
object_id,
|
||||
item_id,
|
||||
key: IndexValue::U64(account_id).serialize(),
|
||||
},
|
||||
)))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
return Ok(RegistryWriteResult::InvalidForeignKey {
|
||||
object_id: *foreign_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
IndexKey::Unique {
|
||||
property,
|
||||
value_1,
|
||||
value_2,
|
||||
global,
|
||||
} => {
|
||||
let key = ValueKey::from(ValueClass::Registry(RegistryClass::PrimaryKey {
|
||||
object_id: (!*global).then_some(object_id),
|
||||
index_id: property.to_id(),
|
||||
key: serialize_composite_key(value_1, value_2),
|
||||
}));
|
||||
if let Some(existing_id) = self
|
||||
.0
|
||||
.store
|
||||
.get_value::<ObjectId>(key)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
&& existing_id != ObjectId::new(object_type, Id::new(item_id))
|
||||
{
|
||||
return Ok(RegistryWriteResult::PrimaryKeyConflict {
|
||||
property: *property,
|
||||
existing_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
IndexKey::Search { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Assign id
|
||||
if generate_id {
|
||||
let mut id_batch = BatchBuilder::new();
|
||||
id_batch.add_and_get(
|
||||
ValueClass::Registry(RegistryClass::IdCounter { object_id }),
|
||||
1,
|
||||
);
|
||||
item_id = self
|
||||
.0
|
||||
.store
|
||||
.write(id_batch.build_all())
|
||||
.await
|
||||
.and_then(|v| v.last_counter_id())? as u64;
|
||||
}
|
||||
|
||||
// It's pickle time!
|
||||
let out = object.inner.to_pickled_vec();
|
||||
if out.len() > MAX_OBJECT_PAYLOAD_SIZE {
|
||||
return Ok(RegistryWriteResult::ValidationError {
|
||||
errors: vec![ValidationError::Invalid {
|
||||
property: Property::Id,
|
||||
value: format!(
|
||||
"Object size {} exceeds maximum of {}",
|
||||
out.len(),
|
||||
MAX_OBJECT_PAYLOAD_SIZE
|
||||
),
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
// Build batch
|
||||
if write_id {
|
||||
batch.set(
|
||||
ValueClass::Registry(RegistryClass::IndexId { object_id, item_id }),
|
||||
vec![],
|
||||
);
|
||||
}
|
||||
|
||||
batch
|
||||
.registry_index(object_id, item_id, set_index.keys.iter(), true)
|
||||
.registry_index(object_id, item_id, clear_index.keys.iter(), false)
|
||||
.set(
|
||||
ValueClass::Registry(RegistryClass::Item { object_id, item_id }),
|
||||
out,
|
||||
);
|
||||
|
||||
self.store()
|
||||
.write(batch.build_all())
|
||||
.await
|
||||
.map(|_| RegistryWriteResult::Success(Id::new(item_id)))
|
||||
}
|
||||
|
||||
async fn delete(
|
||||
&self,
|
||||
object_id: ObjectId,
|
||||
object: Option<&Object>,
|
||||
allowed_orphan_types: &[ObjectType],
|
||||
) -> trc::Result<RegistryWriteResult> {
|
||||
let object_type = object_id.object();
|
||||
let object_type_id = object_type.to_id();
|
||||
let id = object_id.id();
|
||||
let item_id = id.id();
|
||||
|
||||
// Fetch object
|
||||
let object = if let Some(object) = object {
|
||||
Cow::Borrowed(object)
|
||||
} else if let Some(object) = self.get(object_id).await? {
|
||||
Cow::Owned(object)
|
||||
} else {
|
||||
return Ok(RegistryWriteResult::NotFound {
|
||||
object_id: ObjectId::new(object_type, id),
|
||||
});
|
||||
};
|
||||
|
||||
// Validate tenant and account changes
|
||||
let mut clear_index = IndexBuilder::default();
|
||||
object.index(&mut clear_index);
|
||||
|
||||
// Validate relationships
|
||||
let mut linked = self.linked_objects(object_id).await?;
|
||||
if !linked.is_empty() {
|
||||
if !allowed_orphan_types.is_empty() {
|
||||
linked.retain(|object_id| !allowed_orphan_types.contains(&object_id.object()));
|
||||
}
|
||||
|
||||
if !linked.is_empty() {
|
||||
return Ok(RegistryWriteResult::CannotDeleteLinked {
|
||||
object_id: ObjectId::new(object_type, id),
|
||||
linked_objects: linked,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Build deletion batch
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
.assert_value(
|
||||
ValueClass::Registry(RegistryClass::Item {
|
||||
object_id: object_type_id,
|
||||
item_id,
|
||||
}),
|
||||
AssertValue::Hash(object.revision),
|
||||
)
|
||||
.clear(ValueClass::Registry(RegistryClass::Item {
|
||||
object_id: object_type_id,
|
||||
item_id,
|
||||
}))
|
||||
.clear(ValueClass::Registry(RegistryClass::IndexId {
|
||||
object_id: object_type_id,
|
||||
item_id,
|
||||
}))
|
||||
.registry_index(object_type_id, item_id, clear_index.keys.iter(), false);
|
||||
|
||||
self.0
|
||||
.store
|
||||
.write(batch.build_all())
|
||||
.await
|
||||
.map(|_| RegistryWriteResult::Success(Id::from(item_id)))
|
||||
.caused_by(trc::location!())
|
||||
}
|
||||
|
||||
pub async fn linked_objects(&self, object_id: ObjectId) -> trc::Result<Vec<ObjectId>> {
|
||||
let object_type_id = object_id.object().to_id();
|
||||
let item_id = object_id.id().id();
|
||||
let mut linked = Vec::new();
|
||||
let from_key = ValueKey::from(ValueClass::Registry(RegistryClass::Reference {
|
||||
to_object_id: object_type_id,
|
||||
to_item_id: item_id,
|
||||
from_object_id: 0,
|
||||
from_item_id: 0,
|
||||
}));
|
||||
let to_key = ValueKey::from(ValueClass::Registry(RegistryClass::Reference {
|
||||
to_object_id: object_type_id,
|
||||
to_item_id: item_id,
|
||||
from_object_id: u16::MAX,
|
||||
from_item_id: u64::MAX,
|
||||
}));
|
||||
|
||||
self.0
|
||||
.store
|
||||
.iterate(
|
||||
IterateParams::new(from_key, to_key).no_values().ascending(),
|
||||
|key, _| {
|
||||
if key.len() == (U16_LEN * 2) + (U64_LEN * 2) {
|
||||
let object =
|
||||
ObjectType::from_id(key.deserialize_be_u16(U64_LEN + U16_LEN)?)
|
||||
.ok_or_else(|| {
|
||||
trc::EventType::Registry(
|
||||
trc::RegistryEvent::DeserializationError,
|
||||
)
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.ctx(trc::Key::Key, key)
|
||||
})?;
|
||||
let id = key.deserialize_be_u64(U64_LEN + U16_LEN + U16_LEN)?;
|
||||
linked.push(ObjectId::new(object, Id::new(id)));
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())
|
||||
.map(|_| linked)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn assign_id(&self) -> u64 {
|
||||
self.0.id_generator.generate()
|
||||
}
|
||||
}
|
||||
|
||||
impl BatchBuilder {
|
||||
pub fn registry_index<'x>(
|
||||
&mut self,
|
||||
object_id: u16,
|
||||
item_id: u64,
|
||||
index_keys: impl Iterator<Item = &'x IndexKey<'x>>,
|
||||
is_set: bool,
|
||||
) -> &mut Self {
|
||||
for key in index_keys {
|
||||
let (key, value) = match key {
|
||||
IndexKey::Search { property, value } => (
|
||||
RegistryClass::Index {
|
||||
index_id: property.to_id(),
|
||||
object_id,
|
||||
item_id,
|
||||
key: value.serialize(),
|
||||
},
|
||||
vec![],
|
||||
),
|
||||
IndexKey::Unique {
|
||||
property,
|
||||
value_1,
|
||||
value_2,
|
||||
global,
|
||||
} => (
|
||||
RegistryClass::PrimaryKey {
|
||||
object_id: (!*global).then_some(object_id),
|
||||
index_id: property.to_id(),
|
||||
key: serialize_composite_key(value_1, value_2),
|
||||
},
|
||||
KeySerializer::new(U16_LEN + U64_LEN)
|
||||
.write(object_id)
|
||||
.write(item_id)
|
||||
.finalize(),
|
||||
),
|
||||
IndexKey::ForeignKey {
|
||||
object_id: to_object_id,
|
||||
..
|
||||
} => (
|
||||
RegistryClass::Reference {
|
||||
to_object_id: to_object_id.object().to_id(),
|
||||
to_item_id: to_object_id.id().id(),
|
||||
from_item_id: item_id,
|
||||
from_object_id: object_id,
|
||||
},
|
||||
vec![],
|
||||
),
|
||||
};
|
||||
if is_set {
|
||||
if !value.is_empty() {
|
||||
self.assert_value(ValueClass::Registry(key.clone()), ());
|
||||
}
|
||||
self.set(ValueClass::Registry(key), value);
|
||||
} else {
|
||||
self.clear(ValueClass::Registry(key));
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_composite_key(value_1: &IndexValue<'_>, value_2: &IndexValue<'_>) -> Vec<u8> {
|
||||
let mut key = value_1.serialize();
|
||||
match value_2 {
|
||||
IndexValue::Text(text) => key.extend_from_slice(text.as_bytes()),
|
||||
IndexValue::Bytes(bytes) => key.extend_from_slice(bytes),
|
||||
IndexValue::U64(num) => key.extend_from_slice(&num.to_be_bytes()),
|
||||
IndexValue::I64(num) => key.extend_from_slice(&num.to_be_bytes()),
|
||||
IndexValue::U16(num) => key.extend_from_slice(&num.to_be_bytes()),
|
||||
IndexValue::None => {}
|
||||
}
|
||||
key
|
||||
}
|
||||
|
||||
impl SerializeInfallible for IndexValue<'_> {
|
||||
fn serialize(&self) -> Vec<u8> {
|
||||
match self {
|
||||
IndexValue::Text(text) => text.as_bytes().to_vec(),
|
||||
IndexValue::Bytes(bytes) => bytes.clone(),
|
||||
IndexValue::U64(num) => num.to_be_bytes().to_vec(),
|
||||
IndexValue::I64(num) => num.to_be_bytes().to_vec(),
|
||||
IndexValue::U16(num) => num.to_be_bytes().to_vec(),
|
||||
IndexValue::None => vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> RegistryWrite<'x> {
|
||||
pub fn insert(object: &'x Object) -> Self {
|
||||
RegistryWrite::Insert { object, id: None }
|
||||
}
|
||||
|
||||
pub fn insert_with_id(id: Id, object: &'x Object) -> Self {
|
||||
RegistryWrite::Insert {
|
||||
object,
|
||||
id: Some(id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(id: Id, object: &'x Object, old_object: &'x Object) -> Self {
|
||||
RegistryWrite::Update {
|
||||
object,
|
||||
id,
|
||||
old_object,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(object_id: ObjectId) -> Self {
|
||||
RegistryWrite::Delete {
|
||||
object_id,
|
||||
object: None,
|
||||
allowed_orphan_types: &[],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_object(object_id: ObjectId, object: &'x Object) -> Self {
|
||||
RegistryWrite::Delete {
|
||||
object_id,
|
||||
object: Some(object),
|
||||
allowed_orphan_types: &[],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RegistryWriteResult {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
RegistryWriteResult::Success(id) => write!(f, "Success: {}", id),
|
||||
RegistryWriteResult::CannotDeleteLinked {
|
||||
object_id,
|
||||
linked_objects,
|
||||
} => {
|
||||
write!(f, "Cannot delete {} because it is linked to: ", object_id)?;
|
||||
for linked in linked_objects {
|
||||
write!(f, "{}, ", linked)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
RegistryWriteResult::InvalidSingletonId => write!(f, "Invalid singleton id"),
|
||||
RegistryWriteResult::CannotDeleteSingleton => write!(f, "Cannot delete singleton"),
|
||||
RegistryWriteResult::NotFound { object_id } => write!(f, "Not found: {}", object_id),
|
||||
RegistryWriteResult::InvalidForeignKey { object_id } => {
|
||||
write!(f, "Invalid foreign key: {}", object_id)
|
||||
}
|
||||
RegistryWriteResult::PrimaryKeyConflict {
|
||||
property,
|
||||
existing_id,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"Primary key conflict on property {:?} with existing object {}",
|
||||
property.as_str(),
|
||||
existing_id
|
||||
)
|
||||
}
|
||||
RegistryWriteResult::ValidationError { errors } => {
|
||||
write!(f, "Validation error: ")?;
|
||||
for error in errors {
|
||||
write!(f, "{}, ", error)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
RegistryWriteResult::NotSupported => write!(f, "Operation not supported"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
IterateParams, Store, U32_LEN, ValueKey,
|
||||
search::*,
|
||||
write::{
|
||||
SEARCH_INDEX_MAX_FIELD_LEN, SearchIndex, SearchIndexClass, SearchIndexField, SearchIndexId,
|
||||
SearchIndexType, ValueClass,
|
||||
key::{DeserializeBigEndian, KeySerializer},
|
||||
},
|
||||
};
|
||||
use ahash::AHashMap;
|
||||
use roaring::RoaringBitmap;
|
||||
use std::{
|
||||
collections::hash_map::Entry,
|
||||
ops::{BitAndAssign, BitOrAssign},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use utils::cheeky_hash::CheekyHash;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct BitmapCache {
|
||||
cache: AHashMap<(CheekyHash, u8), Option<RoaringBitmap>>,
|
||||
}
|
||||
|
||||
impl BitmapCache {
|
||||
pub async fn merge_bitmaps(
|
||||
&mut self,
|
||||
store: &Store,
|
||||
index: SearchIndex,
|
||||
account_id: u32,
|
||||
hashes: impl Iterator<Item = CheekyHash>,
|
||||
field: u8,
|
||||
is_union: bool,
|
||||
) -> trc::Result<Option<RoaringBitmap>> {
|
||||
let mut result = RoaringBitmap::new();
|
||||
for (idx, hash) in hashes.enumerate() {
|
||||
match self.cache.entry((hash, field)) {
|
||||
Entry::Occupied(entry) => {
|
||||
if let Some(bm) = entry.get() {
|
||||
if is_union {
|
||||
result.bitor_assign(bm);
|
||||
} else if idx == 0 {
|
||||
result = bm.clone();
|
||||
} else {
|
||||
result.bitand_assign(bm);
|
||||
if result.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
} else if !is_union {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
let from_key = ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: 0,
|
||||
},
|
||||
typ: SearchIndexType::Term { hash, field },
|
||||
}));
|
||||
let to_key = ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: u32::MAX,
|
||||
},
|
||||
typ: SearchIndexType::Term { hash, field },
|
||||
}));
|
||||
let key_len = (U32_LEN * 2) + hash.len() + 2;
|
||||
let mut documents = RoaringBitmap::new();
|
||||
store
|
||||
.iterate(
|
||||
IterateParams::new(from_key, to_key).no_values().ascending(),
|
||||
|key, _| {
|
||||
if key.len() == key_len {
|
||||
documents.insert(key.deserialize_be_u32(key.len() - U32_LEN)?);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if !documents.is_empty() {
|
||||
if is_union {
|
||||
result.bitor_assign(&documents);
|
||||
} else if idx == 0 {
|
||||
result = documents.clone();
|
||||
} else {
|
||||
result.bitand_assign(&documents);
|
||||
if result.is_empty() {
|
||||
entry.insert(Some(documents));
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
entry.insert(Some(documents));
|
||||
} else if !is_union {
|
||||
entry.insert(None);
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !result.is_empty() {
|
||||
Ok(Some(result))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn range_to_bitmap(
|
||||
store: &Store,
|
||||
index: SearchIndex,
|
||||
account_id: u32,
|
||||
field_id: u8,
|
||||
match_value: &[u8],
|
||||
op: SearchOperator,
|
||||
) -> trc::Result<Option<RoaringBitmap>> {
|
||||
let ((from_value, from_doc_id, from_field), (end_value, end_doc_id, end_field)) = match op {
|
||||
SearchOperator::LowerThan => ((&[][..], 0, field_id), (match_value, 0, field_id)),
|
||||
SearchOperator::LowerEqualThan => {
|
||||
((&[][..], 0, field_id), (match_value, u32::MAX, field_id))
|
||||
}
|
||||
SearchOperator::GreaterThan => (
|
||||
(match_value, u32::MAX, field_id),
|
||||
(&[][..], u32::MAX, field_id + 1),
|
||||
),
|
||||
SearchOperator::GreaterEqualThan => (
|
||||
(match_value, 0, field_id),
|
||||
(&[][..], u32::MAX, field_id + 1),
|
||||
),
|
||||
SearchOperator::Equal | SearchOperator::Contains => (
|
||||
(match_value, 0, field_id),
|
||||
(match_value, u32::MAX, field_id),
|
||||
),
|
||||
};
|
||||
|
||||
let begin = ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: from_doc_id,
|
||||
},
|
||||
typ: SearchIndexType::Index {
|
||||
field: SearchIndexField {
|
||||
field_id: from_field,
|
||||
data: from_value.to_vec(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
let end = ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: end_doc_id,
|
||||
},
|
||||
typ: SearchIndexType::Index {
|
||||
field: SearchIndexField {
|
||||
field_id: end_field,
|
||||
data: end_value.to_vec(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
let mut bm = RoaringBitmap::new();
|
||||
let prefix = KeySerializer::new(U32_LEN + 2)
|
||||
.write(index.as_u8() | 1 << 6)
|
||||
.write(account_id)
|
||||
.write(field_id)
|
||||
.finalize();
|
||||
let prefix_len = prefix.len();
|
||||
|
||||
store
|
||||
.iterate(
|
||||
IterateParams::new(begin, end).no_values().ascending(),
|
||||
|key, _| {
|
||||
if !key.starts_with(&prefix) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let id_pos = key.len() - U32_LEN;
|
||||
let value = key
|
||||
.get(prefix_len..id_pos)
|
||||
.ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?;
|
||||
|
||||
let matches = match op {
|
||||
SearchOperator::LowerThan => value < match_value,
|
||||
SearchOperator::LowerEqualThan => value <= match_value,
|
||||
SearchOperator::GreaterThan => value > match_value,
|
||||
SearchOperator::GreaterEqualThan => value >= match_value,
|
||||
SearchOperator::Equal | SearchOperator::Contains => value == match_value,
|
||||
};
|
||||
|
||||
if matches {
|
||||
bm.insert(key.deserialize_be_u32(id_pos)?);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if !bm.is_empty() {
|
||||
Ok(Some(bm))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn sort_order(
|
||||
store: &Store,
|
||||
index: SearchIndex,
|
||||
account_id: u32,
|
||||
field_id: u8,
|
||||
) -> trc::Result<AHashMap<u32, u32>> {
|
||||
let begin = ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: 0,
|
||||
},
|
||||
typ: SearchIndexType::Index {
|
||||
field: SearchIndexField {
|
||||
field_id,
|
||||
data: vec![0u8],
|
||||
},
|
||||
},
|
||||
}));
|
||||
let end = ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: u32::MAX,
|
||||
},
|
||||
typ: SearchIndexType::Index {
|
||||
field: SearchIndexField {
|
||||
field_id,
|
||||
data: vec![u8::MAX; SEARCH_INDEX_MAX_FIELD_LEN],
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
let mut last_value = Vec::new();
|
||||
let mut results = AHashMap::new();
|
||||
let mut pos = 0;
|
||||
store
|
||||
.iterate(
|
||||
IterateParams::new(begin, end).no_values().ascending(),
|
||||
|key, _| {
|
||||
let value = key
|
||||
.get(U32_LEN + 2..key.len() - U32_LEN)
|
||||
.ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?;
|
||||
if value != last_value {
|
||||
pos += 1;
|
||||
last_value = value.to_vec();
|
||||
}
|
||||
|
||||
results.insert(key.deserialize_be_u32(key.len() - U32_LEN)?, pos);
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
IterateParams, Store, U64_LEN, ValueKey,
|
||||
search::*,
|
||||
write::{
|
||||
SearchIndex, SearchIndexClass, SearchIndexField, SearchIndexId, SearchIndexType,
|
||||
ValueClass,
|
||||
key::{DeserializeBigEndian, KeySerializer},
|
||||
},
|
||||
};
|
||||
use ahash::AHashMap;
|
||||
use roaring::RoaringTreemap;
|
||||
use std::{
|
||||
collections::hash_map::Entry,
|
||||
ops::{BitAndAssign, BitOrAssign},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use utils::cheeky_hash::CheekyHash;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct TreemapCache {
|
||||
cache: AHashMap<(CheekyHash, u8), Option<RoaringTreemap>>,
|
||||
}
|
||||
|
||||
impl TreemapCache {
|
||||
pub async fn merge_treemaps(
|
||||
&mut self,
|
||||
store: &Store,
|
||||
index: SearchIndex,
|
||||
hashes: impl Iterator<Item = CheekyHash>,
|
||||
field: u8,
|
||||
is_union: bool,
|
||||
) -> trc::Result<Option<RoaringTreemap>> {
|
||||
let mut result = RoaringTreemap::new();
|
||||
for (idx, hash) in hashes.enumerate() {
|
||||
match self.cache.entry((hash, field)) {
|
||||
Entry::Occupied(entry) => {
|
||||
if let Some(bm) = entry.get() {
|
||||
if is_union {
|
||||
result.bitor_assign(bm);
|
||||
} else if idx == 0 {
|
||||
result = bm.clone();
|
||||
} else {
|
||||
result.bitand_assign(bm);
|
||||
if result.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
} else if !is_union {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
let from_key = ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Global { id: 0 },
|
||||
typ: SearchIndexType::Term { hash, field },
|
||||
}));
|
||||
let to_key = ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Global { id: u64::MAX },
|
||||
typ: SearchIndexType::Term { hash, field },
|
||||
}));
|
||||
let key_len = U64_LEN + hash.len() + 2;
|
||||
let mut documents = RoaringTreemap::new();
|
||||
store
|
||||
.iterate(
|
||||
IterateParams::new(from_key, to_key).no_values().ascending(),
|
||||
|key, _| {
|
||||
if key.len() == key_len {
|
||||
documents.insert(key.deserialize_be_u64(key.len() - U64_LEN)?);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if !documents.is_empty() {
|
||||
if is_union {
|
||||
result.bitor_assign(&documents);
|
||||
} else if idx == 0 {
|
||||
result = documents.clone();
|
||||
} else {
|
||||
result.bitand_assign(&documents);
|
||||
if result.is_empty() {
|
||||
entry.insert(Some(documents));
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
entry.insert(Some(documents));
|
||||
} else if !is_union {
|
||||
entry.insert(None);
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !result.is_empty() {
|
||||
Ok(Some(result))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn range_to_treemap(
|
||||
store: &Store,
|
||||
index: SearchIndex,
|
||||
field_id: u8,
|
||||
match_value: &[u8],
|
||||
op: SearchOperator,
|
||||
) -> trc::Result<Option<RoaringTreemap>> {
|
||||
let ((from_value, from_id, from_field), (end_value, end_id, end_field)) = match op {
|
||||
SearchOperator::LowerThan => ((&[][..], 0, field_id), (match_value, 0, field_id)),
|
||||
SearchOperator::LowerEqualThan => {
|
||||
((&[][..], 0, field_id), (match_value, u64::MAX, field_id))
|
||||
}
|
||||
SearchOperator::GreaterThan => (
|
||||
(match_value, u64::MAX, field_id),
|
||||
(&[][..], u64::MAX, field_id + 1),
|
||||
),
|
||||
SearchOperator::GreaterEqualThan => (
|
||||
(match_value, 0, field_id),
|
||||
(&[][..], u64::MAX, field_id + 1),
|
||||
),
|
||||
SearchOperator::Equal | SearchOperator::Contains => (
|
||||
(match_value, 0, field_id),
|
||||
(match_value, u64::MAX, field_id),
|
||||
),
|
||||
};
|
||||
|
||||
let begin = ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Global { id: from_id },
|
||||
typ: SearchIndexType::Index {
|
||||
field: SearchIndexField {
|
||||
field_id: from_field,
|
||||
data: from_value.to_vec(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
let end = ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Global { id: end_id },
|
||||
typ: SearchIndexType::Index {
|
||||
field: SearchIndexField {
|
||||
field_id: end_field,
|
||||
data: end_value.to_vec(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
let mut bm = RoaringTreemap::new();
|
||||
let prefix = KeySerializer::new(U64_LEN + 2)
|
||||
.write(index.as_u8() | 1 << 6)
|
||||
.write(field_id)
|
||||
.finalize();
|
||||
let prefix_len = prefix.len();
|
||||
|
||||
store
|
||||
.iterate(
|
||||
IterateParams::new(begin, end).no_values().ascending(),
|
||||
|key, _| {
|
||||
if !key.starts_with(&prefix) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let id_pos = key.len() - U64_LEN;
|
||||
let value = key
|
||||
.get(prefix_len..id_pos)
|
||||
.ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?;
|
||||
|
||||
let matches = match op {
|
||||
SearchOperator::LowerThan => value < match_value,
|
||||
SearchOperator::LowerEqualThan => value <= match_value,
|
||||
SearchOperator::GreaterThan => value > match_value,
|
||||
SearchOperator::GreaterEqualThan => value >= match_value,
|
||||
SearchOperator::Equal | SearchOperator::Contains => value == match_value,
|
||||
};
|
||||
|
||||
if matches {
|
||||
bm.insert(key.deserialize_be_u64(id_pos)?);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if !bm.is_empty() {
|
||||
Ok(Some(bm))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::search::*;
|
||||
|
||||
impl IndexDocument {
|
||||
pub fn new(index: SearchIndex) -> Self {
|
||||
Self {
|
||||
fields: Default::default(),
|
||||
index,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_account_id(mut self, account_id: u32) -> Self {
|
||||
self.fields
|
||||
.insert(SearchField::AccountId, SearchValue::Uint(account_id as u64));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_document_id(mut self, document_id: u32) -> Self {
|
||||
self.fields.insert(
|
||||
SearchField::DocumentId,
|
||||
SearchValue::Uint(document_id as u64),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_id(mut self, id: u64) -> Self {
|
||||
self.fields.insert(SearchField::Id, SearchValue::Uint(id));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn index_text(&mut self, field: impl Into<SearchField>, value: &str, language: Language) {
|
||||
match self.fields.entry(field.into()) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
if let SearchValue::Text {
|
||||
value: existing_value,
|
||||
..
|
||||
} = entry.get_mut()
|
||||
{
|
||||
existing_value.push(' ');
|
||||
sanitize_text_to_buf(existing_value, value);
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(SearchValue::Text {
|
||||
value: sanitize_text(value),
|
||||
language,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn index_bool(&mut self, field: impl Into<SearchField>, value: bool) {
|
||||
self.fields
|
||||
.insert(field.into(), SearchValue::Boolean(value));
|
||||
}
|
||||
|
||||
pub fn index_integer<N: Into<i64>>(&mut self, field: impl Into<SearchField>, value: N) {
|
||||
self.fields
|
||||
.insert(field.into(), SearchValue::Int(value.into()));
|
||||
}
|
||||
|
||||
pub fn index_unsigned<N: Into<u64>>(&mut self, field: impl Into<SearchField>, value: N) {
|
||||
self.fields
|
||||
.insert(field.into(), SearchValue::Uint(value.into()));
|
||||
}
|
||||
|
||||
pub fn index_keyword(&mut self, field: impl Into<SearchField>, value: impl AsRef<str>) {
|
||||
self.fields.insert(
|
||||
field.into(),
|
||||
SearchValue::Text {
|
||||
value: sanitize_text(value.as_ref()),
|
||||
language: Language::None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn insert_key_value(
|
||||
&mut self,
|
||||
field: impl Into<SearchField>,
|
||||
key: impl AsRef<str>,
|
||||
value: impl AsRef<str>,
|
||||
) {
|
||||
let search_field = field.into();
|
||||
let key = key
|
||||
.as_ref()
|
||||
.chars()
|
||||
.filter(|ch| !ch.is_control())
|
||||
.map(|ch| ch.to_ascii_lowercase())
|
||||
.collect::<String>();
|
||||
let value = value.as_ref();
|
||||
|
||||
match self.fields.entry(search_field) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
if let SearchValue::KeyValues(existing_key_values) = entry.get_mut() {
|
||||
if let Some(existing_value) = existing_key_values.get_mut(&key) {
|
||||
existing_value.push(' ');
|
||||
sanitize_text_to_buf(existing_value, value);
|
||||
} else {
|
||||
existing_key_values.append(key, sanitize_text(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
let mut new_key_values = VecMap::new();
|
||||
new_key_values.append(key, sanitize_text(value));
|
||||
entry.insert(SearchValue::KeyValues(new_key_values));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.fields.is_empty()
|
||||
}
|
||||
|
||||
pub fn has_field(&self, field: &SearchField) -> bool {
|
||||
self.fields.contains_key(field)
|
||||
}
|
||||
|
||||
pub fn fields(&self) -> impl Iterator<Item = (&SearchField, &SearchValue)> {
|
||||
self.fields.iter()
|
||||
}
|
||||
|
||||
pub fn set_unknown_language(&mut self, lang: Language) {
|
||||
for value in self.fields.values_mut() {
|
||||
if let SearchValue::Text { language, .. } = value
|
||||
&& language.is_unknown()
|
||||
{
|
||||
*language = lang;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchFilter {
|
||||
pub fn cond(
|
||||
field: impl Into<SearchField>,
|
||||
op: SearchOperator,
|
||||
value: impl Into<SearchValue>,
|
||||
) -> Self {
|
||||
SearchFilter::Operator {
|
||||
field: field.into(),
|
||||
op,
|
||||
value: value.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eq(field: impl Into<SearchField>, value: impl Into<SearchValue>) -> Self {
|
||||
SearchFilter::Operator {
|
||||
field: field.into(),
|
||||
op: SearchOperator::Equal,
|
||||
value: value.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lt(field: impl Into<SearchField>, value: impl Into<SearchValue>) -> Self {
|
||||
SearchFilter::Operator {
|
||||
field: field.into(),
|
||||
op: SearchOperator::LowerThan,
|
||||
value: value.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn le(field: impl Into<SearchField>, value: impl Into<SearchValue>) -> Self {
|
||||
SearchFilter::Operator {
|
||||
field: field.into(),
|
||||
op: SearchOperator::LowerEqualThan,
|
||||
value: value.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gt(field: impl Into<SearchField>, value: impl Into<SearchValue>) -> Self {
|
||||
SearchFilter::Operator {
|
||||
field: field.into(),
|
||||
op: SearchOperator::GreaterThan,
|
||||
value: value.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ge(field: impl Into<SearchField>, value: impl Into<SearchValue>) -> Self {
|
||||
SearchFilter::Operator {
|
||||
field: field.into(),
|
||||
op: SearchOperator::GreaterEqualThan,
|
||||
value: value.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_text_detect(
|
||||
field: impl Into<SearchField>,
|
||||
text: impl Into<String>,
|
||||
default_language: Language,
|
||||
) -> Self {
|
||||
let (text, language) = Language::detect(text.into(), default_language);
|
||||
Self::has_text(field, text, language)
|
||||
}
|
||||
|
||||
pub fn has_text(
|
||||
field: impl Into<SearchField>,
|
||||
text: impl Into<String>,
|
||||
language: Language,
|
||||
) -> Self {
|
||||
let text = text.into();
|
||||
let (is_exact, text) = if let Some(text) = text
|
||||
.strip_prefix('"')
|
||||
.and_then(|t| t.strip_suffix('"'))
|
||||
.or_else(|| text.strip_prefix('\'').and_then(|t| t.strip_suffix('\'')))
|
||||
{
|
||||
(true, text.to_string())
|
||||
} else {
|
||||
(false, text)
|
||||
};
|
||||
|
||||
if !matches!(language, Language::None) && is_exact {
|
||||
SearchFilter::Operator {
|
||||
field: field.into(),
|
||||
op: SearchOperator::Equal,
|
||||
value: SearchValue::Text {
|
||||
value: text,
|
||||
language,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
SearchFilter::Operator {
|
||||
field: field.into(),
|
||||
op: SearchOperator::Contains,
|
||||
value: SearchValue::Text {
|
||||
value: text,
|
||||
language,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn has_english_text(field: impl Into<SearchField>, text: impl Into<String>) -> Self {
|
||||
Self::has_text(field, text, Language::English)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn has_keyword(field: impl Into<SearchField>, text: impl Into<String>) -> Self {
|
||||
Self::has_text(field, text, Language::None)
|
||||
}
|
||||
|
||||
pub fn is_in_set(set: RoaringBitmap) -> Self {
|
||||
SearchFilter::DocumentSet(set)
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchComparator {
|
||||
pub fn field(field: impl Into<SearchField>, ascending: bool) -> Self {
|
||||
Self::Field {
|
||||
field: field.into(),
|
||||
ascending,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set(set: RoaringBitmap, ascending: bool) -> Self {
|
||||
Self::DocumentSet { set, ascending }
|
||||
}
|
||||
|
||||
pub fn sorted_set(set: AHashMap<u32, u32>, ascending: bool) -> Self {
|
||||
Self::SortedSet { set, ascending }
|
||||
}
|
||||
|
||||
pub fn ascending(field: impl Into<SearchField>) -> Self {
|
||||
Self::Field {
|
||||
field: field.into(),
|
||||
ascending: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn descending(field: impl Into<SearchField>) -> Self {
|
||||
Self::Field {
|
||||
field: field.into(),
|
||||
ascending: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn write_sanitized(out: &mut String, text: &str) {
|
||||
let mut last_is_space = true;
|
||||
for ch in text.chars() {
|
||||
match ch {
|
||||
' ' | '\x09'..='\x0d' => {
|
||||
if !last_is_space {
|
||||
out.push(' ');
|
||||
last_is_space = true;
|
||||
}
|
||||
}
|
||||
'\0'..='\x1f' | '\x7f'..='\u{9f}' => {}
|
||||
ch => {
|
||||
out.push(ch);
|
||||
last_is_space = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn sanitize_text_to_buf(out: &mut String, text: &str) {
|
||||
out.reserve_exact(text.len());
|
||||
write_sanitized(out, text);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn sanitize_text(text: &str) -> String {
|
||||
let mut out = String::with_capacity(text.len());
|
||||
write_sanitized(&mut out, text);
|
||||
out
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::search::*;
|
||||
|
||||
impl SearchableField for EmailSearchField {
|
||||
fn index() -> SearchIndex {
|
||||
SearchIndex::Email
|
||||
}
|
||||
|
||||
fn primary_keys() -> &'static [SearchField] {
|
||||
&[SearchField::AccountId, SearchField::DocumentId]
|
||||
}
|
||||
|
||||
fn all_fields() -> &'static [SearchField] {
|
||||
&[
|
||||
SearchField::Email(EmailSearchField::From),
|
||||
SearchField::Email(EmailSearchField::To),
|
||||
SearchField::Email(EmailSearchField::Cc),
|
||||
SearchField::Email(EmailSearchField::Bcc),
|
||||
SearchField::Email(EmailSearchField::Subject),
|
||||
SearchField::Email(EmailSearchField::Body),
|
||||
SearchField::Email(EmailSearchField::Attachment),
|
||||
SearchField::Email(EmailSearchField::ReceivedAt),
|
||||
SearchField::Email(EmailSearchField::SentAt),
|
||||
SearchField::Email(EmailSearchField::Size),
|
||||
SearchField::Email(EmailSearchField::HasAttachment),
|
||||
SearchField::Email(EmailSearchField::Headers),
|
||||
]
|
||||
}
|
||||
|
||||
fn is_indexed(&self) -> bool {
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
{
|
||||
matches!(
|
||||
self,
|
||||
EmailSearchField::From
|
||||
| EmailSearchField::To
|
||||
| EmailSearchField::Subject
|
||||
| EmailSearchField::ReceivedAt
|
||||
| EmailSearchField::SentAt
|
||||
| EmailSearchField::Size
|
||||
| EmailSearchField::HasAttachment,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "test_mode")]
|
||||
{
|
||||
matches!(
|
||||
self,
|
||||
EmailSearchField::From
|
||||
| EmailSearchField::To
|
||||
| EmailSearchField::Subject
|
||||
| EmailSearchField::ReceivedAt
|
||||
| EmailSearchField::SentAt
|
||||
| EmailSearchField::Size
|
||||
| EmailSearchField::HasAttachment
|
||||
| EmailSearchField::Bcc
|
||||
| EmailSearchField::Cc
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_text(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
EmailSearchField::From
|
||||
| EmailSearchField::To
|
||||
| EmailSearchField::Cc
|
||||
| EmailSearchField::Bcc
|
||||
| EmailSearchField::Subject
|
||||
| EmailSearchField::Body
|
||||
| EmailSearchField::Attachment,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchableField for CalendarSearchField {
|
||||
fn index() -> SearchIndex {
|
||||
SearchIndex::Calendar
|
||||
}
|
||||
|
||||
fn primary_keys() -> &'static [SearchField] {
|
||||
&[SearchField::AccountId, SearchField::DocumentId]
|
||||
}
|
||||
|
||||
fn all_fields() -> &'static [SearchField] {
|
||||
&[
|
||||
SearchField::Calendar(CalendarSearchField::Title),
|
||||
SearchField::Calendar(CalendarSearchField::Description),
|
||||
SearchField::Calendar(CalendarSearchField::Location),
|
||||
SearchField::Calendar(CalendarSearchField::Owner),
|
||||
SearchField::Calendar(CalendarSearchField::Attendee),
|
||||
SearchField::Calendar(CalendarSearchField::Start),
|
||||
SearchField::Calendar(CalendarSearchField::Uid),
|
||||
]
|
||||
}
|
||||
|
||||
fn is_indexed(&self) -> bool {
|
||||
matches!(self, CalendarSearchField::Start | CalendarSearchField::Uid)
|
||||
}
|
||||
|
||||
fn is_text(&self) -> bool {
|
||||
!self.is_indexed()
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchableField for ContactSearchField {
|
||||
fn index() -> SearchIndex {
|
||||
SearchIndex::Contacts
|
||||
}
|
||||
|
||||
fn primary_keys() -> &'static [SearchField] {
|
||||
&[SearchField::AccountId, SearchField::DocumentId]
|
||||
}
|
||||
|
||||
fn all_fields() -> &'static [SearchField] {
|
||||
&[
|
||||
SearchField::Contact(ContactSearchField::Member),
|
||||
SearchField::Contact(ContactSearchField::Kind),
|
||||
SearchField::Contact(ContactSearchField::Name),
|
||||
SearchField::Contact(ContactSearchField::Nickname),
|
||||
SearchField::Contact(ContactSearchField::Organization),
|
||||
SearchField::Contact(ContactSearchField::Email),
|
||||
SearchField::Contact(ContactSearchField::Phone),
|
||||
SearchField::Contact(ContactSearchField::OnlineService),
|
||||
SearchField::Contact(ContactSearchField::Address),
|
||||
SearchField::Contact(ContactSearchField::Note),
|
||||
SearchField::Contact(ContactSearchField::Uid),
|
||||
]
|
||||
}
|
||||
|
||||
fn is_indexed(&self) -> bool {
|
||||
matches!(self, ContactSearchField::Uid | ContactSearchField::Kind)
|
||||
}
|
||||
|
||||
fn is_text(&self) -> bool {
|
||||
!self.is_indexed()
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchableField for FileSearchField {
|
||||
fn index() -> SearchIndex {
|
||||
SearchIndex::File
|
||||
}
|
||||
|
||||
fn primary_keys() -> &'static [SearchField] {
|
||||
&[SearchField::AccountId, SearchField::DocumentId]
|
||||
}
|
||||
|
||||
fn all_fields() -> &'static [SearchField] {
|
||||
&[
|
||||
SearchField::File(FileSearchField::Name),
|
||||
SearchField::File(FileSearchField::Content),
|
||||
]
|
||||
}
|
||||
|
||||
fn is_indexed(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_text(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchableField for TracingSearchField {
|
||||
fn index() -> SearchIndex {
|
||||
SearchIndex::Tracing
|
||||
}
|
||||
|
||||
fn primary_keys() -> &'static [SearchField] {
|
||||
&[SearchField::Id]
|
||||
}
|
||||
|
||||
fn all_fields() -> &'static [SearchField] {
|
||||
&[
|
||||
SearchField::Tracing(TracingSearchField::EventType),
|
||||
SearchField::Tracing(TracingSearchField::QueueId),
|
||||
SearchField::Tracing(TracingSearchField::Keywords),
|
||||
]
|
||||
}
|
||||
|
||||
fn is_indexed(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
TracingSearchField::QueueId | TracingSearchField::EventType
|
||||
)
|
||||
}
|
||||
|
||||
fn is_text(&self) -> bool {
|
||||
matches!(self, TracingSearchField::Keywords)
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchField {
|
||||
pub(crate) fn is_indexed(&self) -> bool {
|
||||
match self {
|
||||
SearchField::Email(field) => field.is_indexed(),
|
||||
SearchField::Calendar(field) => field.is_indexed(),
|
||||
SearchField::Contact(field) => field.is_indexed(),
|
||||
SearchField::File(field) => field.is_indexed(),
|
||||
SearchField::Tracing(field) => field.is_indexed(),
|
||||
SearchField::AccountId | SearchField::DocumentId | SearchField::Id => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_text(&self) -> bool {
|
||||
match self {
|
||||
SearchField::Email(field) => field.is_text(),
|
||||
SearchField::Calendar(field) => field.is_text(),
|
||||
SearchField::Contact(field) => field.is_text(),
|
||||
SearchField::File(field) => field.is_text(),
|
||||
SearchField::Tracing(field) => field.is_text(),
|
||||
SearchField::AccountId | SearchField::DocumentId | SearchField::Id => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_json(&self) -> bool {
|
||||
matches!(self, SearchField::Email(EmailSearchField::Headers))
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchIndex {
|
||||
pub fn all_fields(&self) -> &[SearchField] {
|
||||
match self {
|
||||
SearchIndex::Email => EmailSearchField::all_fields(),
|
||||
SearchIndex::Calendar => CalendarSearchField::all_fields(),
|
||||
SearchIndex::Contacts => ContactSearchField::all_fields(),
|
||||
SearchIndex::File => FileSearchField::all_fields(),
|
||||
SearchIndex::Tracing => TracingSearchField::all_fields(),
|
||||
SearchIndex::InMemory => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn primary_keys(&self) -> &'static [SearchField] {
|
||||
match self {
|
||||
SearchIndex::Email => EmailSearchField::primary_keys(),
|
||||
SearchIndex::Calendar => CalendarSearchField::primary_keys(),
|
||||
SearchIndex::Contacts => ContactSearchField::primary_keys(),
|
||||
SearchIndex::File => FileSearchField::primary_keys(),
|
||||
SearchIndex::Tracing => TracingSearchField::primary_keys(),
|
||||
SearchIndex::InMemory => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
Deserialize, IterateParams, Store, U64_LEN, ValueKey,
|
||||
search::{
|
||||
IndexDocument, SearchField, SearchFilter, SearchOperator, SearchQuery, SearchValue,
|
||||
term::{TermIndex, TermIndexBuilder},
|
||||
},
|
||||
write::{
|
||||
AlignedBytes, Archive, BatchBuilder, SEARCH_INDEX_MAX_FIELD_LEN, SearchIndex,
|
||||
SearchIndexClass, SearchIndexField, SearchIndexId, SearchIndexType, ValueClass,
|
||||
key::DeserializeBigEndian,
|
||||
},
|
||||
};
|
||||
use ahash::AHashMap;
|
||||
use trc::AddContext;
|
||||
use utils::cheeky_hash::CheekyHash;
|
||||
|
||||
impl Store {
|
||||
pub(crate) async fn index(&self, documents: Vec<IndexDocument>) -> trc::Result<()> {
|
||||
let truncate_at = if self.is_foundationdb() { 1_048_576 } else { 0 };
|
||||
|
||||
for document in documents {
|
||||
let mut batch = BatchBuilder::new();
|
||||
let index = document.index;
|
||||
let mut old_term_index = None;
|
||||
|
||||
if matches!(index, SearchIndex::Calendar | SearchIndex::Contacts) {
|
||||
let mut account_id = None;
|
||||
let mut document_id = None;
|
||||
for (field, value) in &document.fields {
|
||||
if let SearchValue::Uint(id) = value {
|
||||
match field {
|
||||
SearchField::AccountId => {
|
||||
account_id = Some(*id as u32);
|
||||
}
|
||||
SearchField::DocumentId => {
|
||||
document_id = Some(*id as u32);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(account_id), Some(document_id)) = (account_id, document_id)
|
||||
&& let Some(archive) = self
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::from(
|
||||
ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id,
|
||||
},
|
||||
typ: SearchIndexType::Document,
|
||||
}),
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
old_term_index = Some(archive);
|
||||
}
|
||||
}
|
||||
|
||||
let term_index_builder = TermIndexBuilder::build(document, truncate_at);
|
||||
if let Some(old_term_index) = old_term_index {
|
||||
let old_term_index = old_term_index
|
||||
.unarchive::<TermIndex>()
|
||||
.caused_by(trc::location!())?;
|
||||
term_index_builder
|
||||
.index
|
||||
.merge_index(&mut batch, index, term_index_builder.id, old_term_index)
|
||||
.caused_by(trc::location!())?;
|
||||
} else {
|
||||
term_index_builder
|
||||
.index
|
||||
.write_index(&mut batch, index, term_index_builder.id)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
let mut commit_points = batch.commit_points();
|
||||
for commit_point in commit_points.iter() {
|
||||
let batch = batch.build_one(commit_point);
|
||||
self.write(batch).await.caused_by(trc::location!())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn unindex(&self, query: SearchQuery) -> trc::Result<()> {
|
||||
let index = query.index;
|
||||
let mut account_documents: AHashMap<u32, Vec<u32>> = AHashMap::new();
|
||||
let mut ids = vec![];
|
||||
let mut to_id = None;
|
||||
let mut last_account_id = None;
|
||||
|
||||
for filter in query.filters {
|
||||
match filter {
|
||||
SearchFilter::Operator { field, op, value } => match (field, value) {
|
||||
(SearchField::AccountId, SearchValue::Uint(id))
|
||||
if op == SearchOperator::Equal =>
|
||||
{
|
||||
last_account_id = Some(id as u32);
|
||||
account_documents.entry(id as u32).or_default();
|
||||
}
|
||||
(SearchField::DocumentId, SearchValue::Uint(id))
|
||||
if op == SearchOperator::Equal && last_account_id.is_some() =>
|
||||
{
|
||||
account_documents
|
||||
.get_mut(&last_account_id.unwrap())
|
||||
.unwrap()
|
||||
.push(id as u32);
|
||||
}
|
||||
(SearchField::Id, SearchValue::Uint(id)) => match op {
|
||||
SearchOperator::LowerThan => {
|
||||
to_id = Some(id.saturating_sub(1));
|
||||
}
|
||||
SearchOperator::LowerEqualThan => {
|
||||
to_id = Some(id);
|
||||
}
|
||||
SearchOperator::Equal => {
|
||||
ids.push(id);
|
||||
}
|
||||
_ => {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.reason("Unsupported operator for Id field"));
|
||||
}
|
||||
},
|
||||
filter => {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details(format!("Unsupported unindex filter {filter:?}")));
|
||||
}
|
||||
},
|
||||
SearchFilter::And | SearchFilter::Or | SearchFilter::End => {}
|
||||
SearchFilter::Not | SearchFilter::DocumentSet(_) => {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details(format!("Unsupported unindex filter {filter:?}")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete by account and document ids
|
||||
for (account_id, document_ids) in account_documents {
|
||||
if !document_ids.is_empty() {
|
||||
for document_id in document_ids {
|
||||
let Some(archive) = self
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::from(
|
||||
ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id,
|
||||
},
|
||||
typ: SearchIndexType::Document,
|
||||
}),
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let term_index = archive
|
||||
.unarchive::<TermIndex>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut batch = BatchBuilder::new();
|
||||
term_index.delete_index(
|
||||
&mut batch,
|
||||
index,
|
||||
SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id,
|
||||
},
|
||||
);
|
||||
self.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
} else {
|
||||
// Delete all documents for the account
|
||||
self.delete_range(
|
||||
ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: 0,
|
||||
},
|
||||
typ: SearchIndexType::Document,
|
||||
})),
|
||||
ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: u32::MAX,
|
||||
},
|
||||
typ: SearchIndexType::Document,
|
||||
})),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
self.delete_range(
|
||||
ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: 0,
|
||||
},
|
||||
typ: SearchIndexType::Index {
|
||||
field: SearchIndexField {
|
||||
field_id: 0,
|
||||
data: vec![0u8],
|
||||
},
|
||||
},
|
||||
})),
|
||||
ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: u32::MAX,
|
||||
},
|
||||
typ: SearchIndexType::Index {
|
||||
field: SearchIndexField {
|
||||
field_id: u8::MAX,
|
||||
data: vec![u8::MAX; SEARCH_INDEX_MAX_FIELD_LEN],
|
||||
},
|
||||
},
|
||||
})),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
self.delete_range(
|
||||
ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: 0,
|
||||
},
|
||||
typ: SearchIndexType::Term {
|
||||
hash: CheekyHash::NULL,
|
||||
field: 0,
|
||||
},
|
||||
})),
|
||||
ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: u32::MAX,
|
||||
},
|
||||
typ: SearchIndexType::Term {
|
||||
hash: CheekyHash::FULL,
|
||||
field: u8::MAX,
|
||||
},
|
||||
})),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete by global ids
|
||||
for id in ids {
|
||||
let Some(archive) = self
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::from(ValueClass::SearchIndex(
|
||||
SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Global { id },
|
||||
typ: SearchIndexType::Document,
|
||||
},
|
||||
)))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let term_index = archive
|
||||
.unarchive::<TermIndex>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut batch = BatchBuilder::new();
|
||||
term_index.delete_index(&mut batch, index, SearchIndexId::Global { id });
|
||||
self.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
// Delete ranges
|
||||
if let Some(to_id) = to_id {
|
||||
let mut batches = Vec::new();
|
||||
self.iterate(
|
||||
IterateParams::new(
|
||||
ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Global { id: 0 },
|
||||
typ: SearchIndexType::Document,
|
||||
})),
|
||||
ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Global { id: to_id },
|
||||
typ: SearchIndexType::Document,
|
||||
})),
|
||||
),
|
||||
|key, value| {
|
||||
let archive = <Archive<AlignedBytes> as Deserialize>::deserialize(value)?;
|
||||
let term_index = archive.unarchive::<TermIndex>()?;
|
||||
let mut batch = BatchBuilder::new();
|
||||
term_index.delete_index(
|
||||
&mut batch,
|
||||
index,
|
||||
SearchIndexId::Global {
|
||||
id: key.deserialize_be_u64(key.len() - U64_LEN)?,
|
||||
},
|
||||
);
|
||||
batches.push(batch);
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
for mut batch in batches {
|
||||
self.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::search::*;
|
||||
use roaring::RoaringBitmap;
|
||||
|
||||
struct State {
|
||||
pub op: SearchFilter,
|
||||
pub bm: Option<RoaringBitmap>,
|
||||
}
|
||||
|
||||
impl SearchQuery {
|
||||
pub fn new(index: SearchIndex) -> Self {
|
||||
Self {
|
||||
index,
|
||||
filters: Vec::new(),
|
||||
comparators: Vec::new(),
|
||||
mask: RoaringBitmap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_filters(mut self, filters: Vec<SearchFilter>) -> Self {
|
||||
if self.filters.is_empty() {
|
||||
self.filters = filters;
|
||||
} else {
|
||||
self.filters.extend(filters);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_comparators(mut self, comparators: Vec<SearchComparator>) -> Self {
|
||||
if self.comparators.is_empty() {
|
||||
self.comparators = comparators;
|
||||
} else {
|
||||
self.comparators.extend(comparators);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_filter(mut self, filter: SearchFilter) -> Self {
|
||||
self.filters.push(filter);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_filter(&mut self, filter: SearchFilter) -> &mut Self {
|
||||
self.filters.push(filter);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_comparator(mut self, comparator: SearchComparator) -> Self {
|
||||
self.comparators.push(comparator);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_mask(mut self, mask: RoaringBitmap) -> Self {
|
||||
self.mask = mask;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_account_id(mut self, account_id: u32) -> Self {
|
||||
self.filters.push(SearchFilter::cond(
|
||||
SearchField::AccountId,
|
||||
SearchOperator::Equal,
|
||||
SearchValue::Uint(account_id as u64),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn filter(self) -> QueryResults {
|
||||
if self.filters.is_empty() {
|
||||
return QueryResults {
|
||||
results: self.mask,
|
||||
comparators: self.comparators,
|
||||
};
|
||||
}
|
||||
let mut state: State = State {
|
||||
op: SearchFilter::And,
|
||||
bm: None,
|
||||
};
|
||||
let mut stack = Vec::new();
|
||||
let mut filters = self.filters.into_iter().peekable();
|
||||
let mask = self.mask;
|
||||
|
||||
while let Some(filter) = filters.next() {
|
||||
let mut result = match filter {
|
||||
SearchFilter::DocumentSet(set) => Some(set),
|
||||
op @ (SearchFilter::And | SearchFilter::Or | SearchFilter::Not) => {
|
||||
stack.push(state);
|
||||
state = State { op, bm: None };
|
||||
continue;
|
||||
}
|
||||
SearchFilter::End => {
|
||||
if let Some(prev_state) = stack.pop() {
|
||||
let bm = state.bm;
|
||||
state = prev_state;
|
||||
bm
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
SearchFilter::Operator { .. } => {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Apply logical operation
|
||||
if let Some(dest) = &mut state.bm {
|
||||
match state.op {
|
||||
SearchFilter::And => {
|
||||
if let Some(result) = result {
|
||||
dest.bitand_assign(result);
|
||||
} else {
|
||||
dest.clear();
|
||||
}
|
||||
}
|
||||
SearchFilter::Or => {
|
||||
if let Some(result) = result {
|
||||
dest.bitor_assign(result);
|
||||
}
|
||||
}
|
||||
SearchFilter::Not => {
|
||||
if let Some(mut result) = result {
|
||||
result.bitxor_assign(&mask);
|
||||
dest.bitand_assign(result);
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else if let Some(ref mut result_) = result {
|
||||
if let SearchFilter::Not = state.op {
|
||||
result_.bitxor_assign(&mask);
|
||||
}
|
||||
state.bm = result;
|
||||
} else if let SearchFilter::Not = state.op {
|
||||
state.bm = Some(mask.clone());
|
||||
} else {
|
||||
state.bm = Some(RoaringBitmap::new());
|
||||
}
|
||||
|
||||
// And short-circuit
|
||||
if matches!(state.op, SearchFilter::And) && state.bm.as_ref().unwrap().is_empty() {
|
||||
while let Some(filter) = filters.peek() {
|
||||
if matches!(filter, SearchFilter::End) {
|
||||
break;
|
||||
} else {
|
||||
filters.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AND with mask
|
||||
let mut results = state.bm.unwrap_or_default();
|
||||
results.bitand_assign(&mask);
|
||||
QueryResults {
|
||||
results,
|
||||
comparators: self.comparators,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QueryResults {
|
||||
pub fn new(results: RoaringBitmap, comparators: Vec<SearchComparator>) -> Self {
|
||||
Self {
|
||||
results,
|
||||
comparators,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_comparators(mut self, comparators: Vec<SearchComparator>) -> Self {
|
||||
if self.comparators.is_empty() {
|
||||
self.comparators = comparators;
|
||||
} else {
|
||||
self.comparators.extend(comparators);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn results(&self) -> &RoaringBitmap {
|
||||
&self.results
|
||||
}
|
||||
|
||||
pub fn update_results(&mut self, results: RoaringBitmap) {
|
||||
self.results = results;
|
||||
}
|
||||
|
||||
pub fn into_bitmap(self) -> RoaringBitmap {
|
||||
self.results
|
||||
}
|
||||
|
||||
pub fn into_sorted(self) -> Vec<u32> {
|
||||
let comparators = self.comparators;
|
||||
let mut results = self.results.into_iter().collect::<Vec<u32>>();
|
||||
|
||||
if !results.is_empty() && !comparators.is_empty() {
|
||||
results.sort_by(|a, b| {
|
||||
for comparator in &comparators {
|
||||
let (a, b, is_ascending) = match comparator {
|
||||
SearchComparator::DocumentSet { set, ascending } => {
|
||||
(set.contains(*a) as u32, set.contains(*b) as u32, *ascending)
|
||||
}
|
||||
SearchComparator::SortedSet { set, ascending } => {
|
||||
let missing = if *ascending { u32::MAX } else { 0 };
|
||||
(
|
||||
*set.get(a).unwrap_or(&missing),
|
||||
*set.get(b).unwrap_or(&missing),
|
||||
*ascending,
|
||||
)
|
||||
}
|
||||
SearchComparator::Field { .. } => continue,
|
||||
};
|
||||
|
||||
let ordering = if is_ascending { a.cmp(&b) } else { b.cmp(&a) };
|
||||
|
||||
if ordering != Ordering::Equal {
|
||||
return ordering;
|
||||
}
|
||||
}
|
||||
Ordering::Equal
|
||||
});
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod bm_u32;
|
||||
pub mod bm_u64;
|
||||
pub mod document;
|
||||
pub mod fields;
|
||||
pub mod index;
|
||||
pub mod local;
|
||||
pub mod query;
|
||||
pub mod split;
|
||||
pub mod term;
|
||||
|
||||
use crate::write::SearchIndex;
|
||||
use ahash::AHashMap;
|
||||
use nlp::language::Language;
|
||||
use roaring::RoaringBitmap;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::fmt::Display;
|
||||
use std::ops::{BitAndAssign, BitOrAssign, BitXorAssign};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SearchOperator {
|
||||
LowerThan,
|
||||
LowerEqualThan,
|
||||
GreaterThan,
|
||||
GreaterEqualThan,
|
||||
Equal,
|
||||
Contains,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum SearchField {
|
||||
AccountId,
|
||||
DocumentId,
|
||||
Id,
|
||||
Email(EmailSearchField),
|
||||
Calendar(CalendarSearchField),
|
||||
Contact(ContactSearchField),
|
||||
File(FileSearchField),
|
||||
Tracing(TracingSearchField),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum EmailSearchField {
|
||||
From,
|
||||
To,
|
||||
Cc,
|
||||
Bcc,
|
||||
Subject,
|
||||
Body,
|
||||
Attachment,
|
||||
ReceivedAt,
|
||||
SentAt,
|
||||
Size,
|
||||
HasAttachment,
|
||||
Headers,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum CalendarSearchField {
|
||||
Title,
|
||||
Description,
|
||||
Location,
|
||||
Owner,
|
||||
Attendee,
|
||||
Start,
|
||||
Uid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum ContactSearchField {
|
||||
Member,
|
||||
Kind,
|
||||
Name,
|
||||
Nickname,
|
||||
Organization,
|
||||
Email,
|
||||
Phone,
|
||||
OnlineService,
|
||||
Address,
|
||||
Note,
|
||||
Uid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum FileSearchField {
|
||||
Name,
|
||||
Content,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum TracingSearchField {
|
||||
EventType,
|
||||
QueueId,
|
||||
Keywords,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SearchValue {
|
||||
Text { value: String, language: Language },
|
||||
KeyValues(VecMap<String, String>),
|
||||
Int(i64),
|
||||
Uint(u64),
|
||||
Boolean(bool),
|
||||
}
|
||||
|
||||
pub trait SearchDocumentId: Sized + Copy + Display {
|
||||
fn from_u64(id: u64) -> Self;
|
||||
fn field() -> SearchField;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SearchQuery {
|
||||
pub(crate) index: SearchIndex,
|
||||
pub(crate) filters: Vec<SearchFilter>,
|
||||
pub(crate) comparators: Vec<SearchComparator>,
|
||||
pub(crate) mask: RoaringBitmap,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Default)]
|
||||
pub enum SearchFilter {
|
||||
Operator {
|
||||
field: SearchField,
|
||||
op: SearchOperator,
|
||||
value: SearchValue,
|
||||
},
|
||||
DocumentSet(RoaringBitmap),
|
||||
And,
|
||||
Or,
|
||||
Not,
|
||||
#[default]
|
||||
End,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SearchComparator {
|
||||
Field {
|
||||
field: SearchField,
|
||||
ascending: bool,
|
||||
},
|
||||
DocumentSet {
|
||||
set: RoaringBitmap,
|
||||
ascending: bool,
|
||||
},
|
||||
SortedSet {
|
||||
set: AHashMap<u32, u32>,
|
||||
ascending: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IndexDocument {
|
||||
pub(crate) index: SearchIndex,
|
||||
pub(crate) fields: AHashMap<SearchField, SearchValue>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct QueryResults {
|
||||
results: RoaringBitmap,
|
||||
comparators: Vec<SearchComparator>,
|
||||
}
|
||||
|
||||
impl From<EmailSearchField> for SearchField {
|
||||
fn from(field: EmailSearchField) -> Self {
|
||||
SearchField::Email(field)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CalendarSearchField> for SearchField {
|
||||
fn from(field: CalendarSearchField) -> Self {
|
||||
SearchField::Calendar(field)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ContactSearchField> for SearchField {
|
||||
fn from(field: ContactSearchField) -> Self {
|
||||
SearchField::Contact(field)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FileSearchField> for SearchField {
|
||||
fn from(field: FileSearchField) -> Self {
|
||||
SearchField::File(field)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TracingSearchField> for SearchField {
|
||||
fn from(field: TracingSearchField) -> Self {
|
||||
SearchField::Tracing(field)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for SearchValue {
|
||||
fn from(value: u64) -> Self {
|
||||
SearchValue::Uint(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for SearchValue {
|
||||
fn from(value: i64) -> Self {
|
||||
SearchValue::Int(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for SearchValue {
|
||||
fn from(value: u32) -> Self {
|
||||
SearchValue::Uint(value as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for SearchValue {
|
||||
fn from(value: i32) -> Self {
|
||||
SearchValue::Int(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for SearchValue {
|
||||
fn from(value: usize) -> Self {
|
||||
SearchValue::Uint(value as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for SearchValue {
|
||||
fn from(value: bool) -> Self {
|
||||
SearchValue::Boolean(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for SearchValue {
|
||||
fn from(value: String) -> Self {
|
||||
SearchValue::Text {
|
||||
value,
|
||||
language: Language::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchDocumentId for u32 {
|
||||
fn from_u64(id: u64) -> Self {
|
||||
id as u32
|
||||
}
|
||||
|
||||
fn field() -> SearchField {
|
||||
SearchField::DocumentId
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchDocumentId for u64 {
|
||||
fn from_u64(id: u64) -> Self {
|
||||
id
|
||||
}
|
||||
|
||||
fn field() -> SearchField {
|
||||
SearchField::Id
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SearchableField: Sized {
|
||||
fn index() -> SearchIndex;
|
||||
fn primary_keys() -> &'static [SearchField];
|
||||
fn all_fields() -> &'static [SearchField];
|
||||
fn is_indexed(&self) -> bool;
|
||||
fn is_text(&self) -> bool;
|
||||
}
|
||||
|
||||
impl Eq for SearchFilter {}
|
||||
|
||||
impl SearchIndex {
|
||||
pub fn index_name(&self) -> &'static str {
|
||||
match self {
|
||||
SearchIndex::Email => "st_email",
|
||||
SearchIndex::Calendar => "st_calendar",
|
||||
SearchIndex::Contacts => "st_contact",
|
||||
SearchIndex::File => "st_file",
|
||||
SearchIndex::Tracing => "st_tracing",
|
||||
SearchIndex::InMemory => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchField {
|
||||
pub fn field_name(&self) -> &'static str {
|
||||
match self {
|
||||
SearchField::AccountId => "acc_id",
|
||||
SearchField::DocumentId => "doc_id",
|
||||
SearchField::Id => "id",
|
||||
SearchField::Email(field) => match field {
|
||||
EmailSearchField::From => "from",
|
||||
EmailSearchField::To => "to",
|
||||
EmailSearchField::Cc => "cc",
|
||||
EmailSearchField::Bcc => "bcc",
|
||||
EmailSearchField::Subject => "subj",
|
||||
EmailSearchField::Body => "body",
|
||||
EmailSearchField::Attachment => "attach",
|
||||
EmailSearchField::ReceivedAt => "rcvd",
|
||||
EmailSearchField::SentAt => "sent",
|
||||
EmailSearchField::Size => "size",
|
||||
EmailSearchField::HasAttachment => "has_att",
|
||||
EmailSearchField::Headers => "headers",
|
||||
},
|
||||
SearchField::Calendar(field) => match field {
|
||||
CalendarSearchField::Title => "title",
|
||||
CalendarSearchField::Description => "desc",
|
||||
CalendarSearchField::Location => "loc",
|
||||
CalendarSearchField::Owner => "owner",
|
||||
CalendarSearchField::Attendee => "attendee",
|
||||
CalendarSearchField::Start => "start",
|
||||
CalendarSearchField::Uid => "uid",
|
||||
},
|
||||
SearchField::Contact(field) => match field {
|
||||
ContactSearchField::Member => "member",
|
||||
ContactSearchField::Kind => "kind",
|
||||
ContactSearchField::Name => "name",
|
||||
ContactSearchField::Nickname => "nick",
|
||||
ContactSearchField::Organization => "org",
|
||||
ContactSearchField::Email => "email",
|
||||
ContactSearchField::Phone => "phone",
|
||||
ContactSearchField::OnlineService => "online",
|
||||
ContactSearchField::Address => "addr",
|
||||
ContactSearchField::Note => "note",
|
||||
ContactSearchField::Uid => "uid",
|
||||
},
|
||||
SearchField::File(field) => match field {
|
||||
FileSearchField::Name => "name",
|
||||
FileSearchField::Content => "content",
|
||||
},
|
||||
SearchField::Tracing(field) => match field {
|
||||
TracingSearchField::EventType => "ev_type",
|
||||
TracingSearchField::QueueId => "queue_id",
|
||||
TracingSearchField::Keywords => "keywords",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
Store,
|
||||
backend::MAX_TOKEN_LENGTH,
|
||||
search::{
|
||||
QueryResults, SearchComparator, SearchField, SearchFilter, SearchOperator, SearchQuery,
|
||||
SearchValue,
|
||||
bm_u32::{BitmapCache, range_to_bitmap, sort_order},
|
||||
bm_u64::{TreemapCache, range_to_treemap},
|
||||
},
|
||||
write::SEARCH_INDEX_MAX_FIELD_LEN,
|
||||
};
|
||||
use nlp::{language::stemmer::Stemmer, tokenizers::space::SpaceTokenizer};
|
||||
use roaring::{RoaringBitmap, RoaringTreemap};
|
||||
use std::ops::{BitAndAssign, BitOrAssign, BitXorAssign};
|
||||
use utils::cheeky_hash::CheekyHash;
|
||||
|
||||
impl Store {
|
||||
pub(crate) async fn query_account(&self, query: SearchQuery) -> trc::Result<Vec<u32>> {
|
||||
struct State {
|
||||
pub op: SearchFilter,
|
||||
pub bm: Option<RoaringBitmap>,
|
||||
}
|
||||
let mut state: State = State {
|
||||
op: SearchFilter::And,
|
||||
bm: None,
|
||||
};
|
||||
let mut stack = Vec::new();
|
||||
let mask = query.mask;
|
||||
let mut bitmaps = BitmapCache::default();
|
||||
let mut account_id = u32::MAX;
|
||||
|
||||
for filter in &query.filters {
|
||||
if let SearchFilter::Operator {
|
||||
field: SearchField::AccountId,
|
||||
value: SearchValue::Uint(id),
|
||||
..
|
||||
} = filter
|
||||
{
|
||||
account_id = *id as u32;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if account_id == u32::MAX {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details("Account ID must be specified before other filters"));
|
||||
}
|
||||
|
||||
let mut results;
|
||||
if query.filters.len() > 1 {
|
||||
let mut filters = query.filters.into_iter().peekable();
|
||||
while let Some(filter) = filters.next() {
|
||||
let mut result = match filter {
|
||||
SearchFilter::Operator { field, op, value } => {
|
||||
if matches!(field, SearchField::AccountId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if field.is_text()
|
||||
&& matches!(op, SearchOperator::Contains | SearchOperator::Equal)
|
||||
{
|
||||
let (value, language) = match value {
|
||||
SearchValue::Text { value, language } => (value, language),
|
||||
_ => {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details("Expected text value for text field"));
|
||||
}
|
||||
};
|
||||
|
||||
if op == SearchOperator::Equal {
|
||||
bitmaps
|
||||
.merge_bitmaps(
|
||||
self,
|
||||
query.index,
|
||||
account_id,
|
||||
language
|
||||
.tokenize_text(&value, MAX_TOKEN_LENGTH)
|
||||
.map(|token| CheekyHash::new(token.word.as_bytes())),
|
||||
field.u8_id(),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
let mut result = RoaringBitmap::new();
|
||||
for token in Stemmer::new(&value, language, MAX_TOKEN_LENGTH) {
|
||||
let mut tokens = Vec::with_capacity(3);
|
||||
tokens.push(CheekyHash::new(token.word.as_bytes()));
|
||||
tokens.push(CheekyHash::new(
|
||||
format!("{}*", token.word).as_bytes(),
|
||||
));
|
||||
if let Some(stemmed_word) = token.stemmed_word {
|
||||
tokens.push(CheekyHash::new(
|
||||
format!("{stemmed_word}*").as_bytes(),
|
||||
));
|
||||
}
|
||||
let union = bitmaps
|
||||
.merge_bitmaps(
|
||||
self,
|
||||
query.index,
|
||||
account_id,
|
||||
tokens.into_iter(),
|
||||
field.u8_id(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
if let Some(union) = union {
|
||||
if result.is_empty() {
|
||||
result = union;
|
||||
} else {
|
||||
result.bitand_assign(&union);
|
||||
if result.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !result.is_empty() {
|
||||
Some(result)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
} else if field.is_json() {
|
||||
let (key, value) = match value {
|
||||
SearchValue::KeyValues(kv) => kv.into_iter().next().unwrap(),
|
||||
_ => {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details("Expected text value for text field"));
|
||||
}
|
||||
};
|
||||
|
||||
if !value.is_empty() {
|
||||
bitmaps
|
||||
.merge_bitmaps(
|
||||
self,
|
||||
query.index,
|
||||
account_id,
|
||||
SpaceTokenizer::new(value.as_str(), MAX_TOKEN_LENGTH).map(
|
||||
|value| {
|
||||
CheekyHash::new(format!("{key} {value}").as_bytes())
|
||||
},
|
||||
),
|
||||
field.u8_id(),
|
||||
true,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
bitmaps
|
||||
.merge_bitmaps(
|
||||
self,
|
||||
query.index,
|
||||
account_id,
|
||||
[CheekyHash::new(key.as_bytes())].into_iter(),
|
||||
field.u8_id(),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
} else if field.is_indexed() {
|
||||
let value = match value {
|
||||
SearchValue::Text { value, .. } => {
|
||||
let mut value = value.into_bytes();
|
||||
value.truncate(SEARCH_INDEX_MAX_FIELD_LEN);
|
||||
value
|
||||
}
|
||||
SearchValue::Int(v) => (v as u64).to_be_bytes().to_vec(),
|
||||
SearchValue::Uint(v) => v.to_be_bytes().to_vec(),
|
||||
SearchValue::Boolean(v) => vec![v as u8],
|
||||
SearchValue::KeyValues(_) => {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details("Expected non key-value for non-text field"));
|
||||
}
|
||||
};
|
||||
|
||||
range_to_bitmap(
|
||||
self,
|
||||
query.index,
|
||||
account_id,
|
||||
field.u8_id(),
|
||||
&value,
|
||||
op,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details(format!("Field {field:?} is not indexed")));
|
||||
}
|
||||
}
|
||||
SearchFilter::DocumentSet(bitmap) => Some(bitmap),
|
||||
op @ (SearchFilter::And | SearchFilter::Or | SearchFilter::Not) => {
|
||||
stack.push(state);
|
||||
state = State { op, bm: None };
|
||||
continue;
|
||||
}
|
||||
SearchFilter::End => {
|
||||
if let Some(prev_state) = stack.pop() {
|
||||
let bm = state.bm;
|
||||
state = prev_state;
|
||||
bm
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Apply logical operation
|
||||
if let Some(dest) = &mut state.bm {
|
||||
match state.op {
|
||||
SearchFilter::And => {
|
||||
if let Some(result) = result {
|
||||
dest.bitand_assign(result);
|
||||
} else {
|
||||
dest.clear();
|
||||
}
|
||||
}
|
||||
SearchFilter::Or => {
|
||||
if let Some(result) = result {
|
||||
dest.bitor_assign(result);
|
||||
}
|
||||
}
|
||||
SearchFilter::Not => {
|
||||
if let Some(mut result) = result {
|
||||
result.bitxor_assign(&mask);
|
||||
dest.bitand_assign(result);
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else if let Some(result_) = &mut result {
|
||||
if let SearchFilter::Not = state.op {
|
||||
result_.bitxor_assign(&mask);
|
||||
}
|
||||
state.bm = result;
|
||||
} else if let SearchFilter::Not = state.op {
|
||||
state.bm = Some(mask.clone());
|
||||
} else {
|
||||
state.bm = Some(RoaringBitmap::new());
|
||||
}
|
||||
|
||||
// And short circuit
|
||||
if matches!(state.op, SearchFilter::And) && state.bm.as_ref().unwrap().is_empty() {
|
||||
while let Some(filter) = filters.peek() {
|
||||
if matches!(filter, SearchFilter::End) {
|
||||
break;
|
||||
} else {
|
||||
filters.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results = state.bm.unwrap_or_default();
|
||||
results.bitand_assign(&mask);
|
||||
} else {
|
||||
results = mask;
|
||||
}
|
||||
|
||||
if results.len() > 1 && !query.comparators.is_empty() {
|
||||
let mut comparators = Vec::with_capacity(query.comparators.len());
|
||||
for comparator in query.comparators {
|
||||
let comparator = match comparator {
|
||||
SearchComparator::Field { field, ascending } => SearchComparator::SortedSet {
|
||||
set: sort_order(self, query.index, account_id, field.u8_id()).await?,
|
||||
ascending,
|
||||
},
|
||||
_ => comparator,
|
||||
};
|
||||
|
||||
comparators.push(comparator);
|
||||
}
|
||||
|
||||
Ok(QueryResults::new(results, comparators).into_sorted())
|
||||
} else {
|
||||
Ok(results.into_iter().collect::<Vec<_>>())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn query_global(&self, query: SearchQuery) -> trc::Result<Vec<u64>> {
|
||||
struct State {
|
||||
pub op: SearchFilter,
|
||||
pub bm: Option<RoaringTreemap>,
|
||||
}
|
||||
let mut state: State = State {
|
||||
op: SearchFilter::And,
|
||||
bm: None,
|
||||
};
|
||||
let mut stack = Vec::new();
|
||||
let mut filters = query.filters.into_iter().peekable();
|
||||
let mut bitmaps = TreemapCache::default();
|
||||
|
||||
while let Some(filter) = filters.next() {
|
||||
let result = match filter {
|
||||
SearchFilter::Operator { field, op, value } => {
|
||||
if field.is_text() {
|
||||
let value = match value {
|
||||
SearchValue::Text { value, .. } => value,
|
||||
_ => {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details("Expected text value for text field"));
|
||||
}
|
||||
};
|
||||
|
||||
bitmaps
|
||||
.merge_treemaps(
|
||||
self,
|
||||
query.index,
|
||||
SpaceTokenizer::new(value.as_str(), MAX_TOKEN_LENGTH)
|
||||
.map(|word| CheekyHash::new(word.as_bytes())),
|
||||
field.u8_id(),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
} else if field.is_indexed() || matches!(field, SearchField::Id) {
|
||||
let value = match value {
|
||||
SearchValue::Text { value, .. } => value.into_bytes(),
|
||||
SearchValue::Int(v) => (v as u64).to_be_bytes().to_vec(),
|
||||
SearchValue::Uint(v) => v.to_be_bytes().to_vec(),
|
||||
SearchValue::Boolean(v) => vec![v as u8],
|
||||
SearchValue::KeyValues(_) => {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details("Expected non key-value for non-text field"));
|
||||
}
|
||||
};
|
||||
|
||||
range_to_treemap(self, query.index, field.u8_id(), &value, op).await?
|
||||
} else {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details(format!("Field {field:?} is not indexed")));
|
||||
}
|
||||
}
|
||||
SearchFilter::DocumentSet(_) | SearchFilter::Not => {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details("Unsupported filter in global search"));
|
||||
}
|
||||
op @ (SearchFilter::And | SearchFilter::Or) => {
|
||||
stack.push(state);
|
||||
state = State { op, bm: None };
|
||||
continue;
|
||||
}
|
||||
SearchFilter::End => {
|
||||
if let Some(prev_state) = stack.pop() {
|
||||
let bm = state.bm;
|
||||
state = prev_state;
|
||||
bm
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Apply logical operation
|
||||
if let Some(dest) = &mut state.bm {
|
||||
match state.op {
|
||||
SearchFilter::And => {
|
||||
if let Some(result) = result {
|
||||
dest.bitand_assign(result);
|
||||
} else {
|
||||
dest.clear();
|
||||
}
|
||||
}
|
||||
SearchFilter::Or => {
|
||||
if let Some(result) = result {
|
||||
dest.bitor_assign(result);
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else if result.is_some() {
|
||||
state.bm = result;
|
||||
} else {
|
||||
state.bm = Some(RoaringTreemap::new());
|
||||
}
|
||||
|
||||
// And short circuit
|
||||
if matches!(state.op, SearchFilter::And) && state.bm.as_ref().unwrap().is_empty() {
|
||||
while let Some(filter) = filters.peek() {
|
||||
if matches!(filter, SearchFilter::End) {
|
||||
break;
|
||||
} else {
|
||||
filters.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if query.comparators.iter().all(|c| {
|
||||
matches!(
|
||||
c,
|
||||
SearchComparator::Field {
|
||||
field: SearchField::Id,
|
||||
ascending: false
|
||||
}
|
||||
)
|
||||
}) {
|
||||
Ok(state
|
||||
.bm
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.collect::<Vec<_>>())
|
||||
} else {
|
||||
Ok(state.bm.unwrap_or_default().into_iter().collect::<Vec<_>>())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,708 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::search::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) enum SplitFilter {
|
||||
Internal(SearchFilter),
|
||||
External(Vec<SearchFilter>),
|
||||
}
|
||||
|
||||
pub(crate) fn split_filters(filters_in: Vec<SearchFilter>) -> Option<Vec<SplitFilter>> {
|
||||
let mut account_id = u64::MAX;
|
||||
let mut filters: Vec<SearchFilter> = Vec::with_capacity(filters_in.len());
|
||||
let mut op_stack = Vec::new();
|
||||
let mut document_sets: AHashMap<usize, RoaringBitmap> = AHashMap::new();
|
||||
let mut operators: AHashMap<usize, Vec<SearchFilter>> = AHashMap::new();
|
||||
|
||||
for filter in filters_in {
|
||||
match filter {
|
||||
op @ (SearchFilter::And | SearchFilter::Or | SearchFilter::Not) => {
|
||||
op_stack.push(op.clone());
|
||||
filters.push(op);
|
||||
}
|
||||
SearchFilter::End => {
|
||||
if let Some(ops) = operators.remove(&op_stack.len()) {
|
||||
filters.extend(ops);
|
||||
}
|
||||
if let Some(docs) = document_sets.remove(&op_stack.len()) {
|
||||
filters.push(SearchFilter::DocumentSet(docs));
|
||||
}
|
||||
filters.push(SearchFilter::End);
|
||||
op_stack.pop()?;
|
||||
}
|
||||
SearchFilter::Operator {
|
||||
field: SearchField::AccountId,
|
||||
value: SearchValue::Uint(id),
|
||||
..
|
||||
} => {
|
||||
account_id = id;
|
||||
}
|
||||
SearchFilter::Operator { .. } => {
|
||||
operators.entry(op_stack.len()).or_default().push(filter);
|
||||
}
|
||||
SearchFilter::DocumentSet(docs) => match document_sets.entry(op_stack.len()) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
if matches!(op_stack.last(), Some(SearchFilter::Or)) {
|
||||
entry.get_mut().bitor_assign(&docs);
|
||||
} else {
|
||||
entry.get_mut().bitand_assign(&docs);
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(docs);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ops) = operators.remove(&0) {
|
||||
filters.extend(ops);
|
||||
}
|
||||
if let Some(docs) = document_sets.remove(&0) {
|
||||
filters.push(SearchFilter::DocumentSet(docs));
|
||||
}
|
||||
|
||||
if account_id == u64::MAX {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut split: Vec<SplitFilter> = Vec::new();
|
||||
let mut i = 0;
|
||||
|
||||
'outer: while i < filters.len() {
|
||||
let mut j = i;
|
||||
let mut depth = 0;
|
||||
|
||||
while j < filters.len() {
|
||||
match &filters[j] {
|
||||
SearchFilter::And | SearchFilter::Or | SearchFilter::Not => {
|
||||
depth += 1;
|
||||
}
|
||||
SearchFilter::End => {
|
||||
depth -= 1;
|
||||
if depth < 0 {
|
||||
if j > i {
|
||||
break;
|
||||
} else {
|
||||
split.push(SplitFilter::Internal(SearchFilter::End));
|
||||
i += 1;
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
SearchFilter::Operator { .. } => {}
|
||||
SearchFilter::DocumentSet(_) => {
|
||||
if depth == 0 && j > i {
|
||||
break;
|
||||
} else {
|
||||
split.push(SplitFilter::Internal(std::mem::take(&mut filters[i])));
|
||||
i += 1;
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
|
||||
let mut external_filters = vec![SearchFilter::Operator {
|
||||
field: SearchField::AccountId,
|
||||
op: SearchOperator::Equal,
|
||||
value: SearchValue::Uint(account_id),
|
||||
}];
|
||||
let add_or =
|
||||
matches!(split.last(), Some(SplitFilter::Internal(SearchFilter::Or))) && j > i + 1;
|
||||
if add_or {
|
||||
external_filters.push(SearchFilter::Or);
|
||||
}
|
||||
external_filters.extend(&mut filters[i..j].iter_mut().map(std::mem::take));
|
||||
if add_or {
|
||||
external_filters.push(SearchFilter::End);
|
||||
}
|
||||
split.push(SplitFilter::External(external_filters));
|
||||
|
||||
i = j;
|
||||
}
|
||||
|
||||
Some(split)
|
||||
}
|
||||
|
||||
// Test cases
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_split_filters_exhaustive() {
|
||||
let test_cases: Vec<(&str, Vec<SearchFilter>, Vec<SplitFilter>)> = vec![
|
||||
// Test 1: Operator followed by document set at depth 0
|
||||
(
|
||||
"Operator then document set at depth 0",
|
||||
vec![account_id(42), other_op("test"), doc_set(&[1, 2, 3])],
|
||||
vec![
|
||||
SplitFilter::External(vec![account_id(42), other_op("test")]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2, 3])),
|
||||
],
|
||||
),
|
||||
// Test 2: Document set followed by operator at depth 0
|
||||
(
|
||||
"Document set then operator at depth 0",
|
||||
vec![account_id(42), doc_set(&[1, 2, 3]), other_op("test")],
|
||||
vec![
|
||||
SplitFilter::External(vec![account_id(42), other_op("test")]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2, 3])),
|
||||
],
|
||||
),
|
||||
// Test 3: Multiple document sets with operator in between
|
||||
(
|
||||
"Multiple document sets at depth 0 with operator",
|
||||
vec![
|
||||
account_id(42),
|
||||
doc_set(&[1, 2]),
|
||||
other_op("middle"),
|
||||
doc_set(&[2, 4]),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::External(vec![account_id(42), other_op("middle")]),
|
||||
SplitFilter::Internal(doc_set(&[2])),
|
||||
],
|
||||
),
|
||||
// Test 4: Document set at depth 0, then AND group
|
||||
(
|
||||
"Document set then AND group",
|
||||
vec![
|
||||
account_id(42),
|
||||
doc_set(&[1, 2]),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
],
|
||||
vec![
|
||||
SplitFilter::External(vec![
|
||||
account_id(42),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2])),
|
||||
],
|
||||
),
|
||||
// Test 5: AND group followed by document set at depth 0
|
||||
(
|
||||
"AND group then document set",
|
||||
vec![
|
||||
account_id(42),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
doc_set(&[1, 2]),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::External(vec![
|
||||
account_id(42),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2])),
|
||||
],
|
||||
),
|
||||
// Test 6: Operator at depth 0, then OR group, then document set
|
||||
(
|
||||
"Operator, OR group, then document set",
|
||||
vec![
|
||||
account_id(42),
|
||||
other_op("pre"),
|
||||
SearchFilter::Or,
|
||||
other_op("a"),
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
doc_set(&[1, 2, 3]),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::External(vec![
|
||||
account_id(42),
|
||||
SearchFilter::Or,
|
||||
other_op("a"),
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
other_op("pre"),
|
||||
]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2, 3])),
|
||||
],
|
||||
),
|
||||
// Test 7: Document set, OR group, operator
|
||||
(
|
||||
"Document set, OR group, operator",
|
||||
vec![
|
||||
account_id(42),
|
||||
doc_set(&[1, 2]),
|
||||
SearchFilter::Or,
|
||||
other_op("a"),
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
other_op("post"),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::External(vec![
|
||||
account_id(42),
|
||||
SearchFilter::Or,
|
||||
other_op("a"),
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
other_op("post"),
|
||||
]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2])),
|
||||
],
|
||||
),
|
||||
// Test 8: Multiple OR branches with document sets between
|
||||
(
|
||||
"Multiple OR branches with document sets between",
|
||||
vec![
|
||||
account_id(42),
|
||||
SearchFilter::Or,
|
||||
other_op("a"),
|
||||
SearchFilter::End,
|
||||
doc_set(&[1, 2]),
|
||||
SearchFilter::Or,
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
doc_set(&[1, 2, 5, 6]),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::External(vec![
|
||||
account_id(42),
|
||||
SearchFilter::Or,
|
||||
other_op("a"),
|
||||
SearchFilter::End,
|
||||
SearchFilter::Or,
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2])),
|
||||
],
|
||||
),
|
||||
// Test 9: Document sets at different depths - depth 0 and inside AND
|
||||
(
|
||||
"Document sets at different depths in AND",
|
||||
vec![
|
||||
account_id(42),
|
||||
doc_set(&[1, 2]),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
doc_set(&[2, 3]),
|
||||
SearchFilter::End,
|
||||
],
|
||||
vec![
|
||||
SplitFilter::Internal(SearchFilter::And),
|
||||
SplitFilter::External(vec![account_id(42), other_op("a")]),
|
||||
SplitFilter::Internal(doc_set(&[2, 3])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::Internal(doc_set(&[1, 2])),
|
||||
],
|
||||
),
|
||||
// Test 10: Operator, AND group with doc set inside, operator
|
||||
(
|
||||
"Operator, AND(operator, doc_set), operator",
|
||||
vec![
|
||||
account_id(42),
|
||||
other_op("pre"),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
doc_set(&[1, 2, 3]),
|
||||
SearchFilter::End,
|
||||
other_op("post"),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::Internal(SearchFilter::And),
|
||||
SplitFilter::External(vec![account_id(42), other_op("a")]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2, 3])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::External(vec![account_id(42), other_op("pre"), other_op("post")]),
|
||||
],
|
||||
),
|
||||
// Test 11: Document set, nested groups, document set
|
||||
(
|
||||
"Doc set, AND(OR(a,b)), doc set",
|
||||
vec![
|
||||
account_id(42),
|
||||
SearchFilter::Or,
|
||||
doc_set(&[1, 2]),
|
||||
other_op("c"),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
doc_set(&[3, 4]),
|
||||
SearchFilter::End,
|
||||
],
|
||||
vec![
|
||||
SplitFilter::Internal(SearchFilter::Or),
|
||||
SplitFilter::External(vec![
|
||||
account_id(42),
|
||||
SearchFilter::Or,
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
other_op("c"),
|
||||
SearchFilter::End,
|
||||
]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2, 3, 4])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
],
|
||||
),
|
||||
// Test 12: OR with nested AND containing document sets, followed by operator
|
||||
(
|
||||
"OR(AND(doc_set, doc_set), operator) followed by operator",
|
||||
vec![
|
||||
account_id(42),
|
||||
SearchFilter::Or,
|
||||
SearchFilter::And,
|
||||
doc_set(&[1, 2]),
|
||||
doc_set(&[2, 3]),
|
||||
SearchFilter::End,
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
other_op("post"),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::Internal(SearchFilter::Or),
|
||||
SplitFilter::Internal(SearchFilter::And),
|
||||
SplitFilter::Internal(doc_set(&[2])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::External(vec![account_id(42), other_op("b")]),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::External(vec![account_id(42), other_op("post")]),
|
||||
],
|
||||
),
|
||||
// Test 13: Complex: doc set, AND group, doc set, OR group, doc set
|
||||
(
|
||||
"Complex: doc, AND, doc, OR, doc",
|
||||
vec![
|
||||
account_id(42),
|
||||
doc_set(&[1, 2, 3]),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
SearchFilter::End,
|
||||
doc_set(&[1, 2, 3, 5]),
|
||||
SearchFilter::Or,
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
doc_set(&[1, 2, 3, 6]),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::External(vec![
|
||||
account_id(42),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
SearchFilter::End,
|
||||
SearchFilter::Or,
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2, 3])),
|
||||
],
|
||||
),
|
||||
// Test 14: Operator, NOT group, document set
|
||||
(
|
||||
"Operator, NOT(operator), document set",
|
||||
vec![
|
||||
account_id(42),
|
||||
other_op("pre"),
|
||||
SearchFilter::Not,
|
||||
other_op("a"),
|
||||
SearchFilter::End,
|
||||
doc_set(&[1, 2]),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::External(vec![
|
||||
account_id(42),
|
||||
SearchFilter::Not,
|
||||
other_op("a"),
|
||||
SearchFilter::End,
|
||||
other_op("pre"),
|
||||
]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2])),
|
||||
],
|
||||
),
|
||||
// Test 15: Document set, NOT group, operator
|
||||
(
|
||||
"Document set, NOT(operator), operator",
|
||||
vec![
|
||||
account_id(42),
|
||||
doc_set(&[1, 2]),
|
||||
SearchFilter::Not,
|
||||
other_op("a"),
|
||||
doc_set(&[3, 4]),
|
||||
SearchFilter::End,
|
||||
other_op("post"),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::Internal(SearchFilter::Not),
|
||||
SplitFilter::External(vec![account_id(42), other_op("a")]),
|
||||
SplitFilter::Internal(doc_set(&[3, 4])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::External(vec![account_id(42), other_op("post")]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2])),
|
||||
],
|
||||
),
|
||||
// Test 16: Alternating doc sets and operators
|
||||
(
|
||||
"Alternating: doc, op, doc, op, doc",
|
||||
vec![
|
||||
account_id(42),
|
||||
doc_set(&[1]),
|
||||
other_op("a"),
|
||||
doc_set(&[1, 2]),
|
||||
other_op("b"),
|
||||
doc_set(&[1, 3]),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::External(vec![account_id(42), other_op("a"), other_op("b")]),
|
||||
SplitFilter::Internal(doc_set(&[1])),
|
||||
],
|
||||
),
|
||||
// Test 17: Multiple operators, then OR group with doc set inside, then doc set
|
||||
(
|
||||
"Multiple ops, OR(op, doc_set), doc",
|
||||
vec![
|
||||
account_id(42),
|
||||
other_op("a"),
|
||||
SearchFilter::Or,
|
||||
other_op("c"),
|
||||
doc_set(&[1, 2]),
|
||||
SearchFilter::End,
|
||||
other_op("b"),
|
||||
doc_set(&[3, 4]),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::Internal(SearchFilter::Or),
|
||||
SplitFilter::External(vec![account_id(42), other_op("c")]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::External(vec![account_id(42), other_op("a"), other_op("b")]),
|
||||
SplitFilter::Internal(doc_set(&[3, 4])),
|
||||
],
|
||||
),
|
||||
// Test 18: Doc set before and after nested OR(AND(op))
|
||||
(
|
||||
"Doc, OR(AND(op)), doc",
|
||||
vec![
|
||||
account_id(42),
|
||||
doc_set(&[1]),
|
||||
SearchFilter::Or,
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
other_op("c"),
|
||||
SearchFilter::End,
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
doc_set(&[2]),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::External(vec![
|
||||
account_id(42),
|
||||
SearchFilter::Or,
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
other_op("c"),
|
||||
SearchFilter::End,
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
]),
|
||||
SplitFilter::Internal(doc_set(&[])),
|
||||
],
|
||||
),
|
||||
// Test 19: AND group with doc set, operator between, OR group with doc set
|
||||
(
|
||||
"AND(op, doc), op, OR(op, doc)",
|
||||
vec![
|
||||
account_id(42),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
doc_set(&[1, 2]),
|
||||
SearchFilter::End,
|
||||
other_op("middle"),
|
||||
SearchFilter::Or,
|
||||
other_op("b"),
|
||||
other_op("c"),
|
||||
doc_set(&[3, 4]),
|
||||
SearchFilter::End,
|
||||
],
|
||||
vec![
|
||||
SplitFilter::Internal(SearchFilter::And),
|
||||
SplitFilter::External(vec![account_id(42), other_op("a")]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::Internal(SearchFilter::Or),
|
||||
SplitFilter::External(vec![
|
||||
account_id(42),
|
||||
SearchFilter::Or,
|
||||
other_op("b"),
|
||||
other_op("c"),
|
||||
SearchFilter::End,
|
||||
]),
|
||||
SplitFilter::Internal(doc_set(&[3, 4])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::External(vec![account_id(42), other_op("middle")]),
|
||||
],
|
||||
),
|
||||
// Test 20: Deep nesting with document sets at multiple levels
|
||||
(
|
||||
"Deep nesting: doc, AND(doc, OR(doc, AND(op, doc)))",
|
||||
vec![
|
||||
account_id(42),
|
||||
doc_set(&[1]),
|
||||
SearchFilter::And,
|
||||
doc_set(&[2]),
|
||||
SearchFilter::Or,
|
||||
doc_set(&[3]),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
doc_set(&[4]),
|
||||
SearchFilter::End,
|
||||
SearchFilter::End,
|
||||
SearchFilter::End,
|
||||
],
|
||||
vec![
|
||||
SplitFilter::Internal(SearchFilter::And),
|
||||
SplitFilter::Internal(SearchFilter::Or),
|
||||
SplitFilter::Internal(SearchFilter::And),
|
||||
SplitFilter::External(vec![account_id(42), other_op("a")]),
|
||||
SplitFilter::Internal(doc_set(&[4])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::Internal(doc_set(&[3])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::Internal(doc_set(&[2])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::Internal(doc_set(&[1])),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
for (description, input, expected) in test_cases {
|
||||
println!("------ Running test: {} ------", description);
|
||||
let result = split_filters(input.clone());
|
||||
assert!(result.is_some(), "Test '{}' returned None", description);
|
||||
|
||||
let result = result.unwrap();
|
||||
if result != expected {
|
||||
print_split_filter_code(&result);
|
||||
}
|
||||
assert_eq!(result, expected, "Test '{description}' failed",);
|
||||
}
|
||||
}
|
||||
|
||||
fn account_id(id: u64) -> SearchFilter {
|
||||
SearchFilter::Operator {
|
||||
field: SearchField::AccountId,
|
||||
op: SearchOperator::Equal,
|
||||
value: SearchValue::Uint(id),
|
||||
}
|
||||
}
|
||||
|
||||
fn other_op(value: &str) -> SearchFilter {
|
||||
SearchFilter::Operator {
|
||||
field: SearchField::DocumentId,
|
||||
op: SearchOperator::Equal,
|
||||
value: SearchValue::Text {
|
||||
value: value.to_string(),
|
||||
language: Language::None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn doc_set(ids: &[u32]) -> SearchFilter {
|
||||
let mut bitmap = RoaringBitmap::new();
|
||||
for id in ids {
|
||||
bitmap.insert(*id);
|
||||
}
|
||||
SearchFilter::DocumentSet(bitmap)
|
||||
}
|
||||
|
||||
fn print_split_filter_code(splits: &[SplitFilter]) {
|
||||
println!("vec![");
|
||||
for split in splits {
|
||||
match split {
|
||||
SplitFilter::Internal(filter) => {
|
||||
print!(" SplitFilter::Internal(");
|
||||
print_search_filter_code(filter, 0);
|
||||
println!("),");
|
||||
}
|
||||
SplitFilter::External(filters) => {
|
||||
println!(" SplitFilter::External(vec![");
|
||||
for filter in filters {
|
||||
print!(" ");
|
||||
print_search_filter_code(filter, 2);
|
||||
println!(",");
|
||||
}
|
||||
println!(" ]),");
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("]");
|
||||
}
|
||||
|
||||
fn print_search_filter_code(filter: &SearchFilter, indent_level: usize) {
|
||||
let indent = " ".repeat(indent_level);
|
||||
match filter {
|
||||
SearchFilter::Operator { field, op, value } => match (field, op, value) {
|
||||
(SearchField::AccountId, SearchOperator::Equal, SearchValue::Uint(id)) => {
|
||||
print!("account_id({})", id);
|
||||
}
|
||||
(
|
||||
SearchField::DocumentId,
|
||||
SearchOperator::Equal,
|
||||
SearchValue::Text { value, .. },
|
||||
) => {
|
||||
print!("other_op(\"{}\")", value);
|
||||
}
|
||||
_ => {
|
||||
println!("SearchFilter::Operator {{");
|
||||
println!("{} field: {:?},", indent, field);
|
||||
println!("{} op: {:?},", indent, op);
|
||||
println!("{} value: {:?},", indent, value);
|
||||
print!("{}}}", indent);
|
||||
}
|
||||
},
|
||||
SearchFilter::DocumentSet(bitmap) => {
|
||||
let ids: Vec<u32> = bitmap.iter().collect();
|
||||
if ids.is_empty() {
|
||||
print!("doc_set(&[])");
|
||||
} else if ids.len() <= 5 {
|
||||
print!("doc_set(&[");
|
||||
for (i, id) in ids.iter().enumerate() {
|
||||
if i > 0 {
|
||||
print!(", ");
|
||||
}
|
||||
print!("{}", id);
|
||||
}
|
||||
print!("])");
|
||||
} else {
|
||||
// For large bitmaps, create inline
|
||||
println!("{{");
|
||||
println!("{} let mut bitmap = RoaringBitmap::new();", indent);
|
||||
for id in ids {
|
||||
println!("{} bitmap.insert({});", indent, id);
|
||||
}
|
||||
print!("{} doc_set_bitmap(bitmap)", indent);
|
||||
println!();
|
||||
print!("{}}}", indent);
|
||||
}
|
||||
}
|
||||
SearchFilter::And => print!("SearchFilter::And"),
|
||||
SearchFilter::Or => print!("SearchFilter::Or"),
|
||||
SearchFilter::Not => print!("SearchFilter::Not"),
|
||||
SearchFilter::End => print!("SearchFilter::End"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
Serialize,
|
||||
backend::MAX_TOKEN_LENGTH,
|
||||
search::*,
|
||||
write::{
|
||||
Archiver, BatchBuilder, SEARCH_INDEX_MAX_FIELD_LEN, SearchIndexClass, SearchIndexField,
|
||||
SearchIndexId, SearchIndexType, ValueClass,
|
||||
},
|
||||
};
|
||||
use ahash::AHashSet;
|
||||
use nlp::{
|
||||
language::stemmer::Stemmer,
|
||||
tokenizers::{space::SpaceTokenizer, word::WordTokenizer},
|
||||
};
|
||||
use utils::{
|
||||
cheeky_hash::{CheekyBTreeMap, CheekyHash},
|
||||
map::bitmap::BitPop,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)]
|
||||
pub(crate) struct TermIndex {
|
||||
terms: Vec<Term>,
|
||||
fields: Vec<SearchIndexField>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)]
|
||||
pub(crate) struct Term {
|
||||
hash: CheekyHash,
|
||||
fields: u32,
|
||||
}
|
||||
|
||||
pub(crate) struct TermIndexBuilder {
|
||||
pub(crate) index: TermIndex,
|
||||
pub(crate) id: SearchIndexId,
|
||||
}
|
||||
|
||||
impl TermIndexBuilder {
|
||||
pub fn build(document: IndexDocument, truncate_at: usize) -> Self {
|
||||
let mut terms: CheekyBTreeMap<u32> = CheekyBTreeMap::new();
|
||||
let mut fields: Vec<SearchIndexField> = Vec::new();
|
||||
let mut account_id = None;
|
||||
let mut document_id = None;
|
||||
let mut id = None;
|
||||
|
||||
for (field, value) in document.fields {
|
||||
match field {
|
||||
SearchField::Id => {
|
||||
if let SearchValue::Uint(v) = value {
|
||||
fields.push(SearchIndexField {
|
||||
field_id: field.u8_id(),
|
||||
data: v.to_be_bytes().to_vec(),
|
||||
});
|
||||
id = Some(v);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
SearchField::AccountId => {
|
||||
if let SearchValue::Uint(v) = value {
|
||||
account_id = Some(v);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
SearchField::DocumentId => {
|
||||
if let SearchValue::Uint(v) = value {
|
||||
document_id = Some(v);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let field = match value {
|
||||
SearchValue::Text { value, language } => {
|
||||
if field.is_text() {
|
||||
let value = if truncate_at > 0 && value.len() > truncate_at {
|
||||
let pos = value.floor_char_boundary(truncate_at);
|
||||
&value[..pos]
|
||||
} else {
|
||||
&value
|
||||
};
|
||||
|
||||
match language {
|
||||
Language::Unknown => {
|
||||
for token in WordTokenizer::new(value, MAX_TOKEN_LENGTH) {
|
||||
terms
|
||||
.entry(CheekyHash::new(token.word.as_bytes()))
|
||||
.or_default()
|
||||
.bit_push(field.u8_id());
|
||||
}
|
||||
}
|
||||
Language::None => {
|
||||
for token in SpaceTokenizer::new(value, MAX_TOKEN_LENGTH) {
|
||||
terms
|
||||
.entry(CheekyHash::new(token.as_bytes()))
|
||||
.or_default()
|
||||
.bit_push(field.u8_id());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
for token in Stemmer::new(value, language, MAX_TOKEN_LENGTH) {
|
||||
terms
|
||||
.entry(CheekyHash::new(token.word.as_bytes()))
|
||||
.or_default()
|
||||
.bit_push(field.u8_id());
|
||||
|
||||
if let Some(stemmed_word) = token.stemmed_word {
|
||||
terms
|
||||
.entry(CheekyHash::new(
|
||||
format!("{}*", stemmed_word).as_bytes(),
|
||||
))
|
||||
.or_default()
|
||||
.bit_push(field.u8_id());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if field.is_indexed() {
|
||||
let mut data = value.into_bytes();
|
||||
data.truncate(SEARCH_INDEX_MAX_FIELD_LEN);
|
||||
|
||||
SearchIndexField {
|
||||
field_id: field.u8_id(),
|
||||
data,
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
SearchValue::KeyValues(map) => {
|
||||
for (key, value) in map {
|
||||
terms
|
||||
.entry(CheekyHash::new(key.as_bytes()))
|
||||
.or_default()
|
||||
.bit_push(field.u8_id());
|
||||
for token in SpaceTokenizer::new(value.as_str(), MAX_TOKEN_LENGTH) {
|
||||
terms
|
||||
.entry(CheekyHash::new(format!("{key} {token}").as_bytes()))
|
||||
.or_default()
|
||||
.bit_push(field.u8_id());
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
SearchValue::Int(v) => SearchIndexField {
|
||||
field_id: field.u8_id(),
|
||||
data: (v as u64).to_be_bytes().to_vec(),
|
||||
},
|
||||
SearchValue::Uint(v) => SearchIndexField {
|
||||
field_id: field.u8_id(),
|
||||
data: v.to_be_bytes().to_vec(),
|
||||
},
|
||||
SearchValue::Boolean(v) => SearchIndexField {
|
||||
field_id: field.u8_id(),
|
||||
data: vec![v as u8],
|
||||
},
|
||||
};
|
||||
|
||||
fields.push(field);
|
||||
}
|
||||
|
||||
TermIndexBuilder {
|
||||
index: TermIndex {
|
||||
terms: terms
|
||||
.into_iter()
|
||||
.map(|(k, v)| Term { hash: k, fields: v })
|
||||
.collect(),
|
||||
fields,
|
||||
},
|
||||
id: match (account_id, document_id, id) {
|
||||
(Some(account_id), Some(document_id), None) => SearchIndexId::Account {
|
||||
account_id: account_id as u32,
|
||||
document_id: document_id as u32,
|
||||
},
|
||||
(None, None, Some(id)) => SearchIndexId::Global { id },
|
||||
_ => {
|
||||
debug_assert!(
|
||||
false,
|
||||
"Invalid combination of AccountId {account_id:?}, DocumentId {document_id:?} and Id {id:?} fields"
|
||||
);
|
||||
SearchIndexId::Global { id: 0 }
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TermIndex {
|
||||
pub fn write_index(
|
||||
self,
|
||||
batch: &mut BatchBuilder,
|
||||
index: SearchIndex,
|
||||
id: SearchIndexId,
|
||||
) -> trc::Result<()> {
|
||||
let archive = Archiver::new(self);
|
||||
batch
|
||||
.set(
|
||||
ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id,
|
||||
typ: SearchIndexType::Document,
|
||||
}),
|
||||
archive.serialize()?,
|
||||
)
|
||||
.commit_point();
|
||||
|
||||
for term in archive.inner.terms {
|
||||
let mut fields = term.fields;
|
||||
while let Some(field) = fields.bit_pop() {
|
||||
batch
|
||||
.set(
|
||||
ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id,
|
||||
typ: SearchIndexType::Term {
|
||||
hash: term.hash,
|
||||
field,
|
||||
},
|
||||
}),
|
||||
vec![],
|
||||
)
|
||||
.commit_point();
|
||||
}
|
||||
}
|
||||
|
||||
for field in archive.inner.fields {
|
||||
batch
|
||||
.set(
|
||||
ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id,
|
||||
typ: SearchIndexType::Index { field },
|
||||
}),
|
||||
vec![],
|
||||
)
|
||||
.commit_point();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn merge_index(
|
||||
self,
|
||||
batch: &mut BatchBuilder,
|
||||
index: SearchIndex,
|
||||
id: SearchIndexId,
|
||||
old_term: &ArchivedTermIndex,
|
||||
) -> trc::Result<()> {
|
||||
let archive = Archiver::new(self);
|
||||
batch
|
||||
.set(
|
||||
ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id,
|
||||
typ: SearchIndexType::Document,
|
||||
}),
|
||||
archive.serialize()?,
|
||||
)
|
||||
.commit_point();
|
||||
|
||||
let mut old_terms = AHashSet::with_capacity(old_term.terms.len());
|
||||
let mut old_fields = AHashSet::with_capacity(old_term.fields.len());
|
||||
for term in old_term.terms.iter() {
|
||||
let mut fields = term.fields.to_native();
|
||||
while let Some(field) = fields.bit_pop() {
|
||||
old_terms.insert(SearchIndexType::Term {
|
||||
hash: term.hash.to_native(),
|
||||
field,
|
||||
});
|
||||
}
|
||||
}
|
||||
for field in old_term.fields.iter() {
|
||||
old_fields.insert(SearchIndexField {
|
||||
field_id: field.field_id,
|
||||
data: field.data.to_vec(),
|
||||
});
|
||||
}
|
||||
|
||||
for term in archive.inner.terms {
|
||||
let mut fields = term.fields;
|
||||
while let Some(field) = fields.bit_pop() {
|
||||
let typ = SearchIndexType::Term {
|
||||
hash: term.hash,
|
||||
field,
|
||||
};
|
||||
|
||||
if !old_terms.remove(&typ) {
|
||||
batch
|
||||
.set(
|
||||
ValueClass::SearchIndex(SearchIndexClass { index, id, typ }),
|
||||
vec![],
|
||||
)
|
||||
.commit_point();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for field in archive.inner.fields {
|
||||
if !old_fields.remove(&field) {
|
||||
batch
|
||||
.set(
|
||||
ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id,
|
||||
typ: SearchIndexType::Index { field },
|
||||
}),
|
||||
vec![],
|
||||
)
|
||||
.commit_point();
|
||||
}
|
||||
}
|
||||
|
||||
for typ in old_terms {
|
||||
batch
|
||||
.clear(ValueClass::SearchIndex(SearchIndexClass { index, id, typ }))
|
||||
.commit_point();
|
||||
}
|
||||
|
||||
for field in old_fields {
|
||||
batch
|
||||
.clear(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id,
|
||||
typ: SearchIndexType::Index { field },
|
||||
}))
|
||||
.commit_point();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ArchivedTermIndex {
|
||||
pub fn delete_index(&self, batch: &mut BatchBuilder, index: SearchIndex, id: SearchIndexId) {
|
||||
batch
|
||||
.clear(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id,
|
||||
typ: SearchIndexType::Document,
|
||||
}))
|
||||
.commit_point();
|
||||
|
||||
for term in self.terms.iter() {
|
||||
let mut fields = term.fields.to_native();
|
||||
while let Some(field) = fields.bit_pop() {
|
||||
batch
|
||||
.clear(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id,
|
||||
typ: SearchIndexType::Term {
|
||||
hash: term.hash.to_native(),
|
||||
field,
|
||||
},
|
||||
}))
|
||||
.commit_point();
|
||||
}
|
||||
}
|
||||
|
||||
for field in self.fields.iter() {
|
||||
batch
|
||||
.clear(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id,
|
||||
typ: SearchIndexType::Index {
|
||||
field: SearchIndexField {
|
||||
field_id: field.field_id,
|
||||
data: field.data.to_vec(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
.commit_point();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchIndex {
|
||||
pub(crate) fn as_u8(&self) -> u8 {
|
||||
match self {
|
||||
SearchIndex::Email => 0,
|
||||
SearchIndex::Calendar => 1,
|
||||
SearchIndex::Contacts => 2,
|
||||
SearchIndex::File => 3,
|
||||
SearchIndex::Tracing => 4,
|
||||
SearchIndex::InMemory => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchField {
|
||||
pub(crate) fn u8_id(&self) -> u8 {
|
||||
match self {
|
||||
SearchField::AccountId => 0,
|
||||
SearchField::DocumentId => 1,
|
||||
SearchField::Id => 2,
|
||||
SearchField::Email(field) => match field {
|
||||
EmailSearchField::From => 3,
|
||||
EmailSearchField::To => 4,
|
||||
EmailSearchField::Cc => 5,
|
||||
EmailSearchField::Bcc => 6,
|
||||
EmailSearchField::Subject => 7,
|
||||
EmailSearchField::Body => 8,
|
||||
EmailSearchField::Attachment => 9,
|
||||
EmailSearchField::ReceivedAt => 10,
|
||||
EmailSearchField::SentAt => 11,
|
||||
EmailSearchField::Size => 12,
|
||||
EmailSearchField::HasAttachment => 13,
|
||||
EmailSearchField::Headers => 14,
|
||||
},
|
||||
SearchField::Calendar(field) => match field {
|
||||
CalendarSearchField::Title => 3,
|
||||
CalendarSearchField::Description => 4,
|
||||
CalendarSearchField::Location => 5,
|
||||
CalendarSearchField::Owner => 6,
|
||||
CalendarSearchField::Attendee => 7,
|
||||
CalendarSearchField::Start => 8,
|
||||
CalendarSearchField::Uid => 9,
|
||||
},
|
||||
SearchField::Contact(field) => match field {
|
||||
ContactSearchField::Member => 3,
|
||||
ContactSearchField::Kind => 4,
|
||||
ContactSearchField::Name => 5,
|
||||
ContactSearchField::Nickname => 6,
|
||||
ContactSearchField::Organization => 7,
|
||||
ContactSearchField::Email => 8,
|
||||
ContactSearchField::Phone => 9,
|
||||
ContactSearchField::OnlineService => 10,
|
||||
ContactSearchField::Address => 11,
|
||||
ContactSearchField::Note => 12,
|
||||
ContactSearchField::Uid => 13,
|
||||
},
|
||||
SearchField::File(field) => match field {
|
||||
FileSearchField::Name => 3,
|
||||
FileSearchField::Content => 4,
|
||||
},
|
||||
SearchField::Tracing(field) => match field {
|
||||
TracingSearchField::EventType => 3,
|
||||
TracingSearchField::QueueId => 4,
|
||||
TracingSearchField::Keywords => 5,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{Archive, ArchiveVersion};
|
||||
use crate::{U32_LEN, U64_LEN};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum AssertValue {
|
||||
U32(u32),
|
||||
U64(u64),
|
||||
Hash(u64),
|
||||
Archive(ArchiveVersion),
|
||||
Some,
|
||||
None,
|
||||
}
|
||||
|
||||
pub trait ToAssertValue {
|
||||
fn to_assert_value(&self) -> AssertValue;
|
||||
}
|
||||
|
||||
impl ToAssertValue for AssertValue {
|
||||
fn to_assert_value(&self) -> AssertValue {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl ToAssertValue for () {
|
||||
fn to_assert_value(&self) -> AssertValue {
|
||||
AssertValue::None
|
||||
}
|
||||
}
|
||||
|
||||
impl ToAssertValue for u64 {
|
||||
fn to_assert_value(&self) -> AssertValue {
|
||||
AssertValue::U64(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToAssertValue for u32 {
|
||||
fn to_assert_value(&self) -> AssertValue {
|
||||
AssertValue::U32(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToAssertValue for Archive<T> {
|
||||
fn to_assert_value(&self) -> AssertValue {
|
||||
AssertValue::Archive(self.version)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToAssertValue for &Archive<T> {
|
||||
fn to_assert_value(&self) -> AssertValue {
|
||||
AssertValue::Archive(self.version)
|
||||
}
|
||||
}
|
||||
|
||||
impl AssertValue {
|
||||
pub fn matches(&self, bytes: &[u8]) -> bool {
|
||||
match self {
|
||||
AssertValue::U32(v) => bytes
|
||||
.get(bytes.len() - U32_LEN..)
|
||||
.is_some_and(|b| b == v.to_be_bytes()),
|
||||
|
||||
AssertValue::U64(v) => bytes
|
||||
.get(bytes.len() - U64_LEN..)
|
||||
.is_some_and(|b| b == v.to_be_bytes()),
|
||||
AssertValue::Hash(v) => xxhash_rust::xxh3::xxh3_64(bytes) == *v,
|
||||
AssertValue::Archive(v) => match v {
|
||||
ArchiveVersion::Versioned { hash, .. } => bytes
|
||||
.get(bytes.len() - U32_LEN - U64_LEN - 1..bytes.len() - U64_LEN - 1)
|
||||
.is_some_and(|b| b == hash.to_be_bytes()),
|
||||
ArchiveVersion::Hashed { hash } => bytes
|
||||
.get(bytes.len() - U32_LEN - 1..bytes.len() - 1)
|
||||
.is_some_and(|b| b == hash.to_be_bytes()),
|
||||
ArchiveVersion::Unversioned => false,
|
||||
},
|
||||
AssertValue::None => false,
|
||||
AssertValue::Some => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_none(&self) -> bool {
|
||||
matches!(self, AssertValue::None)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,556 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{
|
||||
Batch, BatchBuilder, ChangedCollection, IntoOperations, Operation, ValueClass, ValueOp,
|
||||
assert::ToAssertValue, log::VanishedItem,
|
||||
};
|
||||
use crate::{
|
||||
SerializeInfallible, U32_LEN,
|
||||
write::{
|
||||
LogCollection, MergeFnc, MergeOperation, Params, SetFnc, SetOperation, TaskQueueClass,
|
||||
},
|
||||
};
|
||||
use registry::{
|
||||
schema::structs::Task,
|
||||
types::{EnumImpl, ObjectImpl},
|
||||
};
|
||||
use types::{
|
||||
collection::{Collection, SyncCollection, VanishedCollection},
|
||||
field::FieldType,
|
||||
};
|
||||
use utils::{map::vec_map::VecMap, snowflake::SnowflakeIdGenerator};
|
||||
|
||||
impl BatchBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ops: Vec::with_capacity(32),
|
||||
current_account_id: None,
|
||||
current_collection: None,
|
||||
current_document_id: None,
|
||||
changes: Default::default(),
|
||||
changed_collections: Default::default(),
|
||||
batch_size: 0,
|
||||
batch_ops: 0,
|
||||
has_assertions: false,
|
||||
commit_points: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_account_id(&mut self, account_id: u32) -> &mut Self {
|
||||
if self
|
||||
.current_account_id
|
||||
.is_none_or(|current_account_id| current_account_id != account_id)
|
||||
{
|
||||
self.current_account_id = account_id.into();
|
||||
self.ops.push(Operation::AccountId { account_id });
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_collection(&mut self, collection: Collection) -> &mut Self {
|
||||
let collection_ = Some(collection);
|
||||
if collection_ != self.current_collection {
|
||||
self.current_collection = collection_;
|
||||
self.ops.push(Operation::Collection { collection });
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_document(&mut self, document_id: u32) -> &mut Self {
|
||||
self.ops.push(Operation::DocumentId { document_id });
|
||||
self.current_document_id = Some(document_id);
|
||||
self.has_assertions = false;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn assert_value(
|
||||
&mut self,
|
||||
class: impl Into<ValueClass>,
|
||||
value: impl ToAssertValue,
|
||||
) -> &mut Self {
|
||||
self.ops.push(Operation::AssertValue {
|
||||
class: class.into(),
|
||||
assert_value: value.to_assert_value(),
|
||||
});
|
||||
self.batch_ops += 1;
|
||||
self.has_assertions = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn index(&mut self, field: impl FieldType, value: impl Into<Vec<u8>>) -> &mut Self {
|
||||
let field = field.into();
|
||||
let value = value.into();
|
||||
let value_len = value.len();
|
||||
|
||||
self.ops.push(Operation::Index {
|
||||
field,
|
||||
key: value,
|
||||
set: true,
|
||||
});
|
||||
self.batch_size += (U32_LEN * 3) + value_len;
|
||||
self.batch_ops += 1;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn unindex(&mut self, field: impl FieldType, value: impl Into<Vec<u8>>) -> &mut Self {
|
||||
let field = field.into();
|
||||
let value = value.into();
|
||||
let value_len = value.len();
|
||||
|
||||
self.ops.push(Operation::Index {
|
||||
field,
|
||||
key: value,
|
||||
set: false,
|
||||
});
|
||||
self.batch_size += (U32_LEN * 3) + value_len;
|
||||
self.batch_ops += 1;
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn tag(&mut self, field: impl FieldType) -> &mut Self {
|
||||
self.index(field, vec![])
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn untag(&mut self, field: impl FieldType) -> &mut Self {
|
||||
self.unindex(field, vec![])
|
||||
}
|
||||
|
||||
pub fn add(&mut self, class: impl Into<ValueClass>, value: i64) -> &mut Self {
|
||||
let class = class.into();
|
||||
self.batch_size += class.serialized_size() + std::mem::size_of::<i64>();
|
||||
self.ops.push(Operation::Value {
|
||||
class,
|
||||
op: ValueOp::AtomicAdd(value),
|
||||
});
|
||||
self.batch_ops += 1;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_and_get(&mut self, class: impl Into<ValueClass>, value: i64) -> &mut Self {
|
||||
let class = class.into();
|
||||
self.batch_size += class.serialized_size() + (std::mem::size_of::<i64>() * 2);
|
||||
self.ops.push(Operation::Value {
|
||||
class,
|
||||
op: ValueOp::AddAndGet(value),
|
||||
});
|
||||
self.batch_ops += 1;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set(&mut self, class: impl Into<ValueClass>, value: impl Into<Vec<u8>>) -> &mut Self {
|
||||
let class = class.into();
|
||||
let value = value.into();
|
||||
self.batch_size += class.serialized_size() + value.len();
|
||||
self.ops.push(Operation::Value {
|
||||
class,
|
||||
op: ValueOp::Set(value),
|
||||
});
|
||||
self.batch_ops += 1;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_fnc(
|
||||
&mut self,
|
||||
class: impl Into<ValueClass>,
|
||||
params: Params,
|
||||
fnc: SetFnc,
|
||||
) -> &mut Self {
|
||||
self.ops.push(Operation::Value {
|
||||
class: class.into(),
|
||||
op: ValueOp::SetFnc(SetOperation { fnc, params }),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn merge_fnc(
|
||||
&mut self,
|
||||
class: impl Into<ValueClass>,
|
||||
params: Params,
|
||||
fnc: MergeFnc,
|
||||
) -> &mut Self {
|
||||
self.ops.push(Operation::Value {
|
||||
class: class.into(),
|
||||
op: ValueOp::MergeFnc(MergeOperation { fnc, params }),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, class: impl Into<ValueClass>) -> &mut Self {
|
||||
let class = class.into();
|
||||
self.batch_size += class.serialized_size();
|
||||
self.ops.push(Operation::Value {
|
||||
class,
|
||||
op: ValueOp::Clear,
|
||||
});
|
||||
self.batch_ops += 1;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn acl_grant(&mut self, grant_account_id: u32, op: Vec<u8>) -> &mut Self {
|
||||
self.batch_size += (U32_LEN * 3) + op.len();
|
||||
self.ops.push(Operation::Value {
|
||||
class: ValueClass::Acl(grant_account_id),
|
||||
op: ValueOp::Set(op),
|
||||
});
|
||||
self.batch_ops += 1;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn acl_revoke(&mut self, grant_account_id: u32) -> &mut Self {
|
||||
self.batch_size += U32_LEN * 3;
|
||||
self.ops.push(Operation::Value {
|
||||
class: ValueClass::Acl(grant_account_id),
|
||||
op: ValueOp::Clear,
|
||||
});
|
||||
self.batch_ops += 1;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn log_item_insert(
|
||||
&mut self,
|
||||
collection: SyncCollection,
|
||||
prefix: Option<u32>,
|
||||
) -> &mut Self {
|
||||
if let (Some(account_id), Some(document_id)) =
|
||||
(self.current_account_id, self.current_document_id)
|
||||
{
|
||||
self.changes.get_mut_or_insert(account_id).log_item_insert(
|
||||
collection,
|
||||
prefix,
|
||||
document_id,
|
||||
);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn log_item_update(
|
||||
&mut self,
|
||||
collection: SyncCollection,
|
||||
prefix: Option<u32>,
|
||||
) -> &mut Self {
|
||||
if let (Some(account_id), Some(document_id)) =
|
||||
(self.current_account_id, self.current_document_id)
|
||||
{
|
||||
self.changes.get_mut_or_insert(account_id).log_item_update(
|
||||
collection,
|
||||
prefix,
|
||||
document_id,
|
||||
);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn log_item_delete(
|
||||
&mut self,
|
||||
collection: SyncCollection,
|
||||
prefix: Option<u32>,
|
||||
) -> &mut Self {
|
||||
if let (Some(account_id), Some(document_id)) =
|
||||
(self.current_account_id, self.current_document_id)
|
||||
{
|
||||
self.changes.get_mut_or_insert(account_id).log_item_delete(
|
||||
collection,
|
||||
prefix,
|
||||
document_id,
|
||||
);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn log_container_insert(&mut self, collection: SyncCollection) -> &mut Self {
|
||||
if let (Some(account_id), Some(document_id)) =
|
||||
(self.current_account_id, self.current_document_id)
|
||||
{
|
||||
self.changes
|
||||
.get_mut_or_insert(account_id)
|
||||
.log_container_insert(collection, document_id);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn log_container_update(&mut self, collection: SyncCollection) -> &mut Self {
|
||||
if let (Some(account_id), Some(document_id)) =
|
||||
(self.current_account_id, self.current_document_id)
|
||||
{
|
||||
self.changes
|
||||
.get_mut_or_insert(account_id)
|
||||
.log_container_update(collection, document_id);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn log_container_delete(&mut self, collection: SyncCollection) -> &mut Self {
|
||||
if let (Some(account_id), Some(document_id)) =
|
||||
(self.current_account_id, self.current_document_id)
|
||||
{
|
||||
self.changes
|
||||
.get_mut_or_insert(account_id)
|
||||
.log_container_delete(collection, document_id);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn log_container_property_change(
|
||||
&mut self,
|
||||
collection: SyncCollection,
|
||||
document_id: u32,
|
||||
) -> &mut Self {
|
||||
if let Some(account_id) = self.current_account_id {
|
||||
self.changes
|
||||
.get_mut_or_insert(account_id)
|
||||
.log_container_property_update(collection, document_id);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn log_vanished_item(
|
||||
&mut self,
|
||||
collection: VanishedCollection,
|
||||
item: impl Into<VanishedItem>,
|
||||
) -> &mut Self {
|
||||
if let Some(account_id) = self.current_account_id {
|
||||
let item = item.into();
|
||||
self.batch_size += item.serialized_size();
|
||||
self.changes
|
||||
.get_mut_or_insert(account_id)
|
||||
.log_vanished_item(collection, item);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn log_share_notification(
|
||||
&mut self,
|
||||
notification_id: u64,
|
||||
notify_account_id: u32,
|
||||
value: impl SerializeInfallible,
|
||||
) -> &mut Self {
|
||||
self.changed_collections
|
||||
.get_mut_or_insert(notify_account_id)
|
||||
.share_notification_id = Some(notification_id);
|
||||
self.set(
|
||||
ValueClass::ShareNotification {
|
||||
notification_id,
|
||||
notify_account_id,
|
||||
},
|
||||
value.serialize(),
|
||||
)
|
||||
}
|
||||
|
||||
fn serialize_changes(&mut self) {
|
||||
if !self.changes.is_empty() {
|
||||
for (account_id, changelog) in std::mem::take(&mut self.changes) {
|
||||
self.with_account_id(account_id);
|
||||
|
||||
// Serialize changes
|
||||
for (collection, changes) in changelog.changes.into_iter() {
|
||||
let cc = self.changed_collections.get_mut_or_insert(account_id);
|
||||
if changes.has_container_changes() {
|
||||
cc.changed_containers.insert(collection);
|
||||
}
|
||||
if changes.has_item_changes() {
|
||||
cc.changed_items.insert(collection);
|
||||
}
|
||||
|
||||
self.ops.push(Operation::Log {
|
||||
collection: LogCollection::Sync(collection),
|
||||
set: changes.serialize(),
|
||||
});
|
||||
}
|
||||
|
||||
// Serialize vanished items
|
||||
for (collection, vanished) in changelog.vanished.into_iter() {
|
||||
self.ops.push(Operation::Log {
|
||||
collection: LogCollection::Vanished(collection),
|
||||
set: vanished.serialize(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn commit_point(&mut self) -> &mut Self {
|
||||
if self.is_large_batch() {
|
||||
self.serialize_changes();
|
||||
self.commit_points.push(self.ops.len());
|
||||
self.batch_ops = 0;
|
||||
self.batch_size = 0;
|
||||
if let Some(account_id) = self.current_account_id {
|
||||
self.ops.push(Operation::AccountId { account_id });
|
||||
}
|
||||
if let Some(collection) = self.current_collection {
|
||||
self.ops.push(Operation::Collection { collection });
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_large_batch(&self) -> bool {
|
||||
self.batch_size > 5_000_000 || self.batch_ops > 1000
|
||||
}
|
||||
|
||||
pub fn any_op(&mut self, op: Operation) -> &mut Self {
|
||||
if let Operation::Value { class, op } = &op {
|
||||
self.batch_size += class.serialized_size();
|
||||
if let ValueOp::Set(value) = op {
|
||||
self.batch_size += value.len();
|
||||
}
|
||||
}
|
||||
|
||||
self.ops.push(op);
|
||||
self.batch_ops += 1;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn custom(&mut self, value: impl IntoOperations) -> trc::Result<&mut Self> {
|
||||
value.build(self)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn last_account_id(&self) -> Option<u32> {
|
||||
self.current_account_id
|
||||
}
|
||||
|
||||
pub fn last_collection(&self) -> Option<Collection> {
|
||||
self.current_collection
|
||||
}
|
||||
|
||||
pub fn last_document_id(&self) -> Option<u32> {
|
||||
self.current_document_id
|
||||
}
|
||||
|
||||
pub fn commit_points(&mut self) -> CommitPointIterator {
|
||||
self.serialize_changes();
|
||||
CommitPointIterator {
|
||||
commit_points: std::mem::take(&mut self.commit_points),
|
||||
commit_point_last: self.ops.len(),
|
||||
offset_start: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_one(&mut self, commit_point: CommitPoint) -> Batch<'_> {
|
||||
Batch {
|
||||
changes: &self.changed_collections,
|
||||
ops: &mut self.ops[commit_point.offset_start..commit_point.offset_end],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_all(&mut self) -> Batch<'_> {
|
||||
self.serialize_changes();
|
||||
Batch {
|
||||
changes: &self.changed_collections,
|
||||
ops: self.ops.as_mut_slice(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn changes(self) -> Option<VecMap<u32, ChangedCollection>> {
|
||||
if self.has_changes() {
|
||||
Some(self.changed_collections)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_changes(&self) -> bool {
|
||||
!self.changed_collections.is_empty()
|
||||
}
|
||||
|
||||
pub fn ops(&self) -> &[Operation] {
|
||||
self.ops.as_slice()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.batch_size
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.batch_ops == 0
|
||||
}
|
||||
|
||||
pub fn schedule_task(&mut self, task: Task) -> &mut Self {
|
||||
let due = task.due_timestamp();
|
||||
let class = task.object_type().to_id();
|
||||
let task = task.to_pickled_vec();
|
||||
let id = SnowflakeIdGenerator::global_id().unwrap_or_default();
|
||||
|
||||
self.set(ValueClass::TaskQueue(TaskQueueClass::Task { id }), task)
|
||||
.set(
|
||||
ValueClass::TaskQueue(TaskQueueClass::Due { id, due }),
|
||||
class.serialize(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn schedule_task_with_id(&mut self, id: u64, task: Task) -> &mut Self {
|
||||
let due = task.due_timestamp();
|
||||
let class = task.object_type().to_id();
|
||||
let task = task.to_pickled_vec();
|
||||
|
||||
self.set(ValueClass::TaskQueue(TaskQueueClass::Task { id }), task)
|
||||
.set(
|
||||
ValueClass::TaskQueue(TaskQueueClass::Due { id, due }),
|
||||
class.serialize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CommitPointIterator {
|
||||
commit_points: Vec<usize>,
|
||||
commit_point_last: usize,
|
||||
offset_start: usize,
|
||||
}
|
||||
|
||||
pub struct CommitPoint {
|
||||
pub offset_start: usize,
|
||||
pub offset_end: usize,
|
||||
}
|
||||
|
||||
impl CommitPointIterator {
|
||||
pub fn iter(&mut self) -> impl Iterator<Item = CommitPoint> {
|
||||
self.commit_points
|
||||
.iter()
|
||||
.copied()
|
||||
.chain([self.commit_point_last])
|
||||
.map(|offset_end| {
|
||||
let point = CommitPoint {
|
||||
offset_start: self.offset_start,
|
||||
offset_end,
|
||||
};
|
||||
self.offset_start = offset_end;
|
||||
point
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Batch<'_> {
|
||||
pub fn is_atomic(&self) -> bool {
|
||||
!self.ops.iter().any(|op| {
|
||||
matches!(
|
||||
op,
|
||||
Operation::AssertValue { .. }
|
||||
| Operation::Value {
|
||||
op: ValueOp::AddAndGet(_),
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn first_account_id(&self) -> Option<u32> {
|
||||
self.ops.iter().find_map(|op| match op {
|
||||
Operation::AccountId { account_id } => Some(*account_id),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BatchBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use bitpacking::{BitPacker, BitPacker1x, BitPacker4x, BitPacker8x};
|
||||
use utils::codec::leb128::Leb128Reader;
|
||||
|
||||
use super::key::KeySerializer;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BitpackIterator<'x> {
|
||||
pub(crate) bytes: &'x [u8],
|
||||
pub(crate) bytes_offset: usize,
|
||||
pub(crate) chunk: Vec<u32>,
|
||||
pub(crate) chunk_offset: usize,
|
||||
pub items_left: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct BitBlockPacker {
|
||||
bitpacker_1: BitPacker1x,
|
||||
bitpacker_4: BitPacker4x,
|
||||
bitpacker_8: BitPacker8x,
|
||||
block_len: usize,
|
||||
}
|
||||
|
||||
impl KeySerializer {
|
||||
pub fn bitpack_sorted(self, items: &[u32]) -> Self {
|
||||
let mut serializer = self;
|
||||
let mut bitpacker = BitBlockPacker::new();
|
||||
let mut compressed = vec![0u8; 4 * BitPacker8x::BLOCK_LEN];
|
||||
|
||||
let mut pos = 0;
|
||||
let len = items.len();
|
||||
let mut initial_value = None;
|
||||
|
||||
serializer = serializer.write_leb128(len as u32);
|
||||
|
||||
while pos < len {
|
||||
let block_len = match len - pos {
|
||||
0..=31 => {
|
||||
for val in &items[pos..] {
|
||||
serializer = serializer.write_leb128(*val);
|
||||
}
|
||||
break;
|
||||
}
|
||||
32..=127 => BitPacker1x::BLOCK_LEN,
|
||||
128..=255 => BitPacker4x::BLOCK_LEN,
|
||||
_ => BitPacker8x::BLOCK_LEN,
|
||||
};
|
||||
|
||||
let chunk = &items[pos..pos + block_len];
|
||||
bitpacker.block_len(block_len);
|
||||
let num_bits: u8 = bitpacker.num_bits_strictly_sorted(initial_value, chunk);
|
||||
let compressed_len = bitpacker.compress_strictly_sorted(
|
||||
initial_value,
|
||||
chunk,
|
||||
&mut compressed[..],
|
||||
num_bits,
|
||||
);
|
||||
serializer = serializer
|
||||
.write(num_bits)
|
||||
.write(&compressed[..compressed_len]);
|
||||
initial_value = chunk[chunk.len() - 1].into();
|
||||
|
||||
pos += block_len;
|
||||
}
|
||||
serializer
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> BitpackIterator<'x> {
|
||||
pub fn from_bytes_and_offset(bytes: &'x [u8], bytes_offset: usize, items_left: u32) -> Self {
|
||||
BitpackIterator {
|
||||
bytes,
|
||||
bytes_offset,
|
||||
items_left,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(bytes: &'x [u8]) -> Option<Self> {
|
||||
bytes
|
||||
.read_leb128::<u32>()
|
||||
.map(|(items_left, bytes_offset)| BitpackIterator {
|
||||
bytes,
|
||||
bytes_offset,
|
||||
items_left,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for BitpackIterator<'_> {
|
||||
type Item = u32;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(item) = self.chunk.get(self.chunk_offset) {
|
||||
self.chunk_offset += 1;
|
||||
return Some(*item);
|
||||
}
|
||||
let block_len = match self.items_left {
|
||||
0 => return None,
|
||||
1..=31 => {
|
||||
self.items_left -= 1;
|
||||
let (item, bytes_read) = self.bytes.get(self.bytes_offset..)?.read_leb128()?;
|
||||
self.bytes_offset += bytes_read;
|
||||
return Some(item);
|
||||
}
|
||||
32..=127 => BitPacker1x::BLOCK_LEN,
|
||||
128..=255 => BitPacker4x::BLOCK_LEN,
|
||||
_ => BitPacker8x::BLOCK_LEN,
|
||||
};
|
||||
|
||||
let bitpacker = BitBlockPacker::with_block_len(block_len);
|
||||
let num_bits = *self.bytes.get(self.bytes_offset)?;
|
||||
let bytes_read = ((num_bits as usize) * block_len / 8) + 1;
|
||||
let initial_value = self.chunk.last().copied();
|
||||
|
||||
self.chunk = vec![0u32; block_len];
|
||||
self.chunk_offset = 1;
|
||||
|
||||
bitpacker.decompress_strictly_sorted(
|
||||
initial_value,
|
||||
self.bytes
|
||||
.get(self.bytes_offset + 1..self.bytes_offset + bytes_read)?,
|
||||
&mut self.chunk[..],
|
||||
num_bits,
|
||||
);
|
||||
|
||||
self.bytes_offset += bytes_read;
|
||||
self.items_left -= block_len as u32;
|
||||
self.chunk.first().copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl BitBlockPacker {
|
||||
pub fn with_block_len(block_len: usize) -> Self {
|
||||
BitBlockPacker {
|
||||
bitpacker_1: BitPacker1x::new(),
|
||||
bitpacker_4: BitPacker4x::new(),
|
||||
bitpacker_8: BitPacker8x::new(),
|
||||
block_len,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn block_len(&mut self, num: usize) {
|
||||
self.block_len = num;
|
||||
}
|
||||
}
|
||||
|
||||
impl BitPacker for BitBlockPacker {
|
||||
const BLOCK_LEN: usize = 0;
|
||||
|
||||
fn new() -> Self {
|
||||
BitBlockPacker {
|
||||
bitpacker_1: BitPacker1x::new(),
|
||||
bitpacker_4: BitPacker4x::new(),
|
||||
bitpacker_8: BitPacker8x::new(),
|
||||
block_len: 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn compress(&self, decompressed: &[u32], compressed: &mut [u8], num_bits: u8) -> usize {
|
||||
match self.block_len {
|
||||
BitPacker8x::BLOCK_LEN => self
|
||||
.bitpacker_8
|
||||
.compress(decompressed, compressed, num_bits),
|
||||
BitPacker4x::BLOCK_LEN => self
|
||||
.bitpacker_4
|
||||
.compress(decompressed, compressed, num_bits),
|
||||
_ => self
|
||||
.bitpacker_1
|
||||
.compress(decompressed, compressed, num_bits),
|
||||
}
|
||||
}
|
||||
|
||||
fn compress_sorted(
|
||||
&self,
|
||||
initial: u32,
|
||||
decompressed: &[u32],
|
||||
compressed: &mut [u8],
|
||||
num_bits: u8,
|
||||
) -> usize {
|
||||
match self.block_len {
|
||||
BitPacker8x::BLOCK_LEN => {
|
||||
self.bitpacker_8
|
||||
.compress_sorted(initial, decompressed, compressed, num_bits)
|
||||
}
|
||||
BitPacker4x::BLOCK_LEN => {
|
||||
self.bitpacker_4
|
||||
.compress_sorted(initial, decompressed, compressed, num_bits)
|
||||
}
|
||||
_ => self
|
||||
.bitpacker_1
|
||||
.compress_sorted(initial, decompressed, compressed, num_bits),
|
||||
}
|
||||
}
|
||||
|
||||
fn decompress(&self, compressed: &[u8], decompressed: &mut [u32], num_bits: u8) -> usize {
|
||||
match self.block_len {
|
||||
BitPacker8x::BLOCK_LEN => {
|
||||
self.bitpacker_8
|
||||
.decompress(compressed, decompressed, num_bits)
|
||||
}
|
||||
BitPacker4x::BLOCK_LEN => {
|
||||
self.bitpacker_4
|
||||
.decompress(compressed, decompressed, num_bits)
|
||||
}
|
||||
_ => self
|
||||
.bitpacker_1
|
||||
.decompress(compressed, decompressed, num_bits),
|
||||
}
|
||||
}
|
||||
|
||||
fn decompress_sorted(
|
||||
&self,
|
||||
initial: u32,
|
||||
compressed: &[u8],
|
||||
decompressed: &mut [u32],
|
||||
num_bits: u8,
|
||||
) -> usize {
|
||||
match self.block_len {
|
||||
BitPacker8x::BLOCK_LEN => {
|
||||
self.bitpacker_8
|
||||
.decompress_sorted(initial, compressed, decompressed, num_bits)
|
||||
}
|
||||
BitPacker4x::BLOCK_LEN => {
|
||||
self.bitpacker_4
|
||||
.decompress_sorted(initial, compressed, decompressed, num_bits)
|
||||
}
|
||||
_ => self
|
||||
.bitpacker_1
|
||||
.decompress_sorted(initial, compressed, decompressed, num_bits),
|
||||
}
|
||||
}
|
||||
|
||||
fn num_bits(&self, decompressed: &[u32]) -> u8 {
|
||||
match self.block_len {
|
||||
BitPacker8x::BLOCK_LEN => self.bitpacker_8.num_bits(decompressed),
|
||||
BitPacker4x::BLOCK_LEN => self.bitpacker_4.num_bits(decompressed),
|
||||
_ => self.bitpacker_1.num_bits(decompressed),
|
||||
}
|
||||
}
|
||||
|
||||
fn num_bits_sorted(&self, initial: u32, decompressed: &[u32]) -> u8 {
|
||||
match self.block_len {
|
||||
BitPacker8x::BLOCK_LEN => self.bitpacker_8.num_bits_sorted(initial, decompressed),
|
||||
BitPacker4x::BLOCK_LEN => self.bitpacker_4.num_bits_sorted(initial, decompressed),
|
||||
_ => self.bitpacker_1.num_bits_sorted(initial, decompressed),
|
||||
}
|
||||
}
|
||||
|
||||
fn compress_strictly_sorted(
|
||||
&self,
|
||||
initial: Option<u32>,
|
||||
decompressed: &[u32],
|
||||
compressed: &mut [u8],
|
||||
num_bits: u8,
|
||||
) -> usize {
|
||||
match self.block_len {
|
||||
BitPacker8x::BLOCK_LEN => self.bitpacker_8.compress_strictly_sorted(
|
||||
initial,
|
||||
decompressed,
|
||||
compressed,
|
||||
num_bits,
|
||||
),
|
||||
BitPacker4x::BLOCK_LEN => self.bitpacker_4.compress_strictly_sorted(
|
||||
initial,
|
||||
decompressed,
|
||||
compressed,
|
||||
num_bits,
|
||||
),
|
||||
_ => self.bitpacker_1.compress_strictly_sorted(
|
||||
initial,
|
||||
decompressed,
|
||||
compressed,
|
||||
num_bits,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn decompress_strictly_sorted(
|
||||
&self,
|
||||
initial: Option<u32>,
|
||||
compressed: &[u8],
|
||||
decompressed: &mut [u32],
|
||||
num_bits: u8,
|
||||
) -> usize {
|
||||
match self.block_len {
|
||||
BitPacker8x::BLOCK_LEN => self.bitpacker_8.decompress_strictly_sorted(
|
||||
initial,
|
||||
compressed,
|
||||
decompressed,
|
||||
num_bits,
|
||||
),
|
||||
BitPacker4x::BLOCK_LEN => self.bitpacker_4.decompress_strictly_sorted(
|
||||
initial,
|
||||
compressed,
|
||||
decompressed,
|
||||
num_bits,
|
||||
),
|
||||
_ => self.bitpacker_1.decompress_strictly_sorted(
|
||||
initial,
|
||||
compressed,
|
||||
decompressed,
|
||||
num_bits,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn num_bits_strictly_sorted(&self, initial: Option<u32>, decompressed: &[u32]) -> u8 {
|
||||
match self.block_len {
|
||||
BitPacker8x::BLOCK_LEN => self
|
||||
.bitpacker_8
|
||||
.num_bits_strictly_sorted(initial, decompressed),
|
||||
BitPacker4x::BLOCK_LEN => self
|
||||
.bitpacker_4
|
||||
.num_bits_strictly_sorted(initial, decompressed),
|
||||
_ => self
|
||||
.bitpacker_1
|
||||
.num_bits_strictly_sorted(initial, decompressed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bitpack_roundtrip() {
|
||||
for num_positions in [
|
||||
1,
|
||||
10,
|
||||
BitPacker1x::BLOCK_LEN,
|
||||
BitPacker4x::BLOCK_LEN,
|
||||
BitPacker8x::BLOCK_LEN,
|
||||
BitPacker8x::BLOCK_LEN + BitPacker4x::BLOCK_LEN + BitPacker1x::BLOCK_LEN,
|
||||
BitPacker8x::BLOCK_LEN + BitPacker4x::BLOCK_LEN + BitPacker1x::BLOCK_LEN + 1,
|
||||
(BitPacker8x::BLOCK_LEN * 3)
|
||||
+ (BitPacker4x::BLOCK_LEN * 3)
|
||||
+ (BitPacker1x::BLOCK_LEN * 3)
|
||||
+ 1,
|
||||
(BitPacker8x::BLOCK_LEN * 32) + 1,
|
||||
] {
|
||||
let serialized = KeySerializer::new(num_positions * std::mem::size_of::<u32>())
|
||||
.bitpack_sorted(
|
||||
&(0..num_positions)
|
||||
.map(|i| (i * i) as u32)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.finalize();
|
||||
|
||||
println!(
|
||||
"Testing block {num_positions} with {} size...",
|
||||
serialized.len()
|
||||
);
|
||||
|
||||
let mut iter = BitpackIterator::new(&serialized).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
iter.items_left, num_positions as u32,
|
||||
"failed for num_positions: {}",
|
||||
num_positions
|
||||
);
|
||||
|
||||
for i in 0..num_positions {
|
||||
assert_eq!(
|
||||
iter.next(),
|
||||
Some((i * i) as u32),
|
||||
"failed for position: {}",
|
||||
i
|
||||
);
|
||||
}
|
||||
assert_eq!(iter.next(), None, "expected end of iterator");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{BlobOp, Operation, ValueClass, ValueOp, key::DeserializeBigEndian, now};
|
||||
use crate::{
|
||||
BlobStore, Deserialize, IterateParams, SerializeInfallible, Store, U16_LEN, U32_LEN, U64_LEN,
|
||||
ValueKey,
|
||||
write::{BatchBuilder, BlobLink, RegistryClass},
|
||||
};
|
||||
use registry::{
|
||||
schema::prelude::Property,
|
||||
types::{EnumImpl, id::ObjectId},
|
||||
};
|
||||
use std::time::Instant;
|
||||
use trc::{AddContext, StoreEvent};
|
||||
use types::{
|
||||
blob::BlobClass,
|
||||
blob_hash::{BLOB_HASH_LEN, BlobHash},
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct BlobQuota {
|
||||
pub bytes: usize,
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
pub async fn blob_exists(&self, hash: impl AsRef<BlobHash> + Sync + Send) -> trc::Result<bool> {
|
||||
self.key_exists(ValueKey {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
class: ValueClass::Blob(BlobOp::Commit {
|
||||
hash: hash.as_ref().clone(),
|
||||
}),
|
||||
})
|
||||
.await
|
||||
.caused_by(trc::location!())
|
||||
}
|
||||
|
||||
pub async fn blob_has_access(
|
||||
&self,
|
||||
hash: impl AsRef<BlobHash> + Sync + Send,
|
||||
class: impl AsRef<BlobClass> + Sync + Send,
|
||||
) -> trc::Result<bool> {
|
||||
let key = match class.as_ref() {
|
||||
BlobClass::Reserved {
|
||||
account_id,
|
||||
expires,
|
||||
} if *expires > now() => ValueKey {
|
||||
account_id: *account_id,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
class: ValueClass::Blob(BlobOp::Link {
|
||||
hash: hash.as_ref().clone(),
|
||||
to: BlobLink::Temporary { until: *expires },
|
||||
}),
|
||||
},
|
||||
BlobClass::Linked {
|
||||
account_id,
|
||||
collection,
|
||||
document_id,
|
||||
} => ValueKey {
|
||||
account_id: *account_id,
|
||||
collection: *collection,
|
||||
document_id: *document_id,
|
||||
class: ValueClass::Blob(BlobOp::Link {
|
||||
hash: hash.as_ref().clone(),
|
||||
to: BlobLink::Document,
|
||||
}),
|
||||
},
|
||||
_ => return Ok(false),
|
||||
};
|
||||
|
||||
self.key_exists(key).await
|
||||
}
|
||||
|
||||
pub async fn purge_blobs_all_shards(&self, blob_store: BlobStore) -> trc::Result<()> {
|
||||
for shard_index in 0u8..=255 {
|
||||
self.purge_blobs(blob_store.clone(), shard_index).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn purge_blobs(&self, blob_store: BlobStore, shard_index: u8) -> trc::Result<()> {
|
||||
let mut total_active = 0;
|
||||
let mut total_deleted = 0;
|
||||
let started = Instant::now();
|
||||
|
||||
// Validate linked blobs
|
||||
let mut from_hash = BlobHash::default();
|
||||
let mut to_hash = BlobHash::new_max();
|
||||
from_hash.0[0] = shard_index;
|
||||
to_hash.0[0] = shard_index;
|
||||
let from_key = ValueKey {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
class: ValueClass::Blob(BlobOp::Commit { hash: from_hash }),
|
||||
};
|
||||
let to_key = ValueKey {
|
||||
account_id: u32::MAX,
|
||||
collection: u8::MAX,
|
||||
document_id: u32::MAX,
|
||||
class: ValueClass::Blob(BlobOp::Link {
|
||||
hash: to_hash,
|
||||
to: BlobLink::Document,
|
||||
}),
|
||||
};
|
||||
|
||||
let mut state = BlobPurgeState::new();
|
||||
self.iterate(
|
||||
IterateParams::new(from_key, to_key).ascending(),
|
||||
|key, value| {
|
||||
let hash =
|
||||
BlobHash::try_from_hash_slice(key.get(0..BLOB_HASH_LEN).ok_or_else(|| {
|
||||
trc::Error::corrupted_key(key, value.into(), trc::location!())
|
||||
})?)
|
||||
.unwrap();
|
||||
|
||||
state.update_hash(hash);
|
||||
state.process_key(key, value)?;
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
state.finalize(BlobHash::default());
|
||||
|
||||
// Delete expired or unlinked blobs
|
||||
for (_, op) in &state.delete_keys {
|
||||
if let BlobOp::Commit { hash } = op {
|
||||
blob_store
|
||||
.delete_blob(hash.as_ref())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete hashes
|
||||
let mut batch = BatchBuilder::new();
|
||||
for (account_id, op) in state.delete_keys {
|
||||
if batch.is_large_batch() {
|
||||
self.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
batch = BatchBuilder::new();
|
||||
}
|
||||
|
||||
if let Some(account_id) = account_id {
|
||||
batch.with_account_id(account_id);
|
||||
}
|
||||
|
||||
batch.any_op(Operation::Value {
|
||||
class: ValueClass::Blob(op),
|
||||
op: ValueOp::Clear,
|
||||
});
|
||||
}
|
||||
for (account_id, object_id) in state.delete_registry {
|
||||
if batch.is_large_batch() {
|
||||
self.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
batch = BatchBuilder::new();
|
||||
}
|
||||
|
||||
let item_id = object_id.id().id();
|
||||
let object_id = object_id.object().to_id();
|
||||
|
||||
batch
|
||||
.clear(ValueClass::Registry(RegistryClass::Index {
|
||||
index_id: Property::AccountId.to_id(),
|
||||
object_id,
|
||||
item_id,
|
||||
key: (account_id as u64).serialize(),
|
||||
}))
|
||||
.clear(ValueClass::Registry(RegistryClass::Item {
|
||||
object_id,
|
||||
item_id,
|
||||
}));
|
||||
}
|
||||
if !batch.is_empty() {
|
||||
self.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
total_active += state.total_active - 1; // Exclude default hash
|
||||
total_deleted += state.total_deleted;
|
||||
|
||||
trc::event!(
|
||||
Store(StoreEvent::BlobStorePurged),
|
||||
Id = shard_index as u16,
|
||||
Expires = total_deleted,
|
||||
Total = total_active,
|
||||
Elapsed = started.elapsed()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct BlobPurgeState {
|
||||
last_hash: BlobHash,
|
||||
last_hash_is_linked: bool,
|
||||
delete_keys: Vec<(Option<u32>, BlobOp)>,
|
||||
delete_registry: Vec<(u32, ObjectId)>,
|
||||
now: u64,
|
||||
total_deleted: u64,
|
||||
total_active: u64,
|
||||
}
|
||||
|
||||
impl BlobPurgeState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
last_hash: BlobHash::default(),
|
||||
last_hash_is_linked: true, // Avoid deleting non-existing last_hash on first iteration
|
||||
delete_keys: Vec::new(),
|
||||
delete_registry: Vec::new(),
|
||||
now: now(),
|
||||
total_deleted: 0,
|
||||
total_active: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_hash(&mut self, hash: BlobHash) {
|
||||
if self.last_hash != hash {
|
||||
self.finalize(hash);
|
||||
self.last_hash_is_linked = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finalize(&mut self, new_hash: BlobHash) {
|
||||
if !self.last_hash_is_linked {
|
||||
self.total_deleted += 1;
|
||||
self.delete_keys.push((
|
||||
None,
|
||||
BlobOp::Commit {
|
||||
hash: std::mem::replace(&mut self.last_hash, new_hash),
|
||||
},
|
||||
));
|
||||
} else {
|
||||
self.total_active += 1;
|
||||
self.last_hash = new_hash;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_key(&mut self, key: &[u8], value: &[u8]) -> trc::Result<()> {
|
||||
const TEMP_LINK: usize = BLOB_HASH_LEN + U32_LEN + U64_LEN;
|
||||
const DOC_LINK: usize = BLOB_HASH_LEN + U64_LEN + 1;
|
||||
const ID_LINK: usize = BLOB_HASH_LEN + U64_LEN;
|
||||
|
||||
match key.len() {
|
||||
BLOB_HASH_LEN => {
|
||||
// Main blob entry
|
||||
Ok(())
|
||||
}
|
||||
TEMP_LINK => {
|
||||
// Temporary link
|
||||
let until = key.deserialize_be_u64(BLOB_HASH_LEN + U32_LEN)?;
|
||||
if until <= self.now {
|
||||
let account_id = key.deserialize_be_u32(BLOB_HASH_LEN)?;
|
||||
self.delete_keys.push((
|
||||
Some(account_id),
|
||||
BlobOp::Link {
|
||||
hash: self.last_hash.clone(),
|
||||
to: BlobLink::Temporary { until },
|
||||
},
|
||||
));
|
||||
if value.len() == U16_LEN + U64_LEN {
|
||||
self.delete_registry
|
||||
.push((account_id, ObjectId::deserialize(value)?));
|
||||
}
|
||||
} else {
|
||||
self.last_hash_is_linked = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
DOC_LINK | ID_LINK => {
|
||||
// Document/Id link
|
||||
self.last_hash_is_linked = true;
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(trc::Error::corrupted_key(
|
||||
key,
|
||||
value.into(),
|
||||
trc::location!(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,685 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{
|
||||
AnyKey, BlobOp, InMemoryClass, QueueClass, TaskQueueClass, TelemetryClass, ValueClass,
|
||||
};
|
||||
use crate::{
|
||||
IndexKey, IndexKeyPrefix, Key, LogKey, SUBSPACE_ACL, SUBSPACE_BLOB_LINK, SUBSPACE_COUNTER,
|
||||
SUBSPACE_DELETED_ITEMS, SUBSPACE_DIRECTORY, SUBSPACE_IN_MEMORY_COUNTER,
|
||||
SUBSPACE_IN_MEMORY_VALUE, SUBSPACE_INDEXES, SUBSPACE_LOGS, SUBSPACE_PROPERTY,
|
||||
SUBSPACE_QUEUE_EVENT, SUBSPACE_QUEUE_MESSAGE, SUBSPACE_QUOTA, SUBSPACE_REGISTRY,
|
||||
SUBSPACE_REGISTRY_IDX, SUBSPACE_REGISTRY_PK, SUBSPACE_REPORT_IN, SUBSPACE_REPORT_OUT,
|
||||
SUBSPACE_SEARCH_INDEX, SUBSPACE_SPAM_SAMPLES, SUBSPACE_TASK_QUEUE, SUBSPACE_TELEMETRY_METRIC,
|
||||
SUBSPACE_TELEMETRY_SPAN, U16_LEN, U32_LEN, U64_LEN, ValueKey, WITH_SUBSPACE,
|
||||
write::{
|
||||
BlobLink, IndexPropertyClass, RegistryClass, SearchIndex, SearchIndexId, SearchIndexType,
|
||||
},
|
||||
};
|
||||
use registry::schema::prelude::ObjectType;
|
||||
use std::convert::TryInto;
|
||||
use types::{
|
||||
blob_hash::BLOB_HASH_LEN,
|
||||
collection::{Collection, SyncCollection},
|
||||
field::{Field, MailboxField},
|
||||
};
|
||||
use utils::codec::leb128::Leb128_;
|
||||
|
||||
pub struct KeySerializer {
|
||||
pub buf: Vec<u8>,
|
||||
}
|
||||
|
||||
pub trait KeySerialize {
|
||||
fn serialize(&self, buf: &mut Vec<u8>);
|
||||
}
|
||||
|
||||
pub trait DeserializeBigEndian {
|
||||
fn deserialize_be_u16(&self, index: usize) -> trc::Result<u16>;
|
||||
fn deserialize_be_u32(&self, index: usize) -> trc::Result<u32>;
|
||||
fn deserialize_be_u64(&self, index: usize) -> trc::Result<u64>;
|
||||
}
|
||||
|
||||
impl KeySerializer {
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
Self {
|
||||
buf: Vec::with_capacity(capacity),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write<T: KeySerialize>(mut self, value: T) -> Self {
|
||||
value.serialize(&mut self.buf);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn write_leb128<T: Leb128_>(mut self, value: T) -> Self {
|
||||
T::to_leb128_bytes(value, &mut self.buf);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn finalize(self) -> Vec<u8> {
|
||||
self.buf
|
||||
}
|
||||
}
|
||||
|
||||
impl KeySerialize for u8 {
|
||||
fn serialize(&self, buf: &mut Vec<u8>) {
|
||||
buf.push(*self);
|
||||
}
|
||||
}
|
||||
|
||||
impl KeySerialize for &str {
|
||||
fn serialize(&self, buf: &mut Vec<u8>) {
|
||||
buf.extend_from_slice(self.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
impl KeySerialize for &String {
|
||||
fn serialize(&self, buf: &mut Vec<u8>) {
|
||||
buf.extend_from_slice(self.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
impl KeySerialize for &[u8] {
|
||||
fn serialize(&self, buf: &mut Vec<u8>) {
|
||||
buf.extend_from_slice(self);
|
||||
}
|
||||
}
|
||||
|
||||
impl KeySerialize for u32 {
|
||||
fn serialize(&self, buf: &mut Vec<u8>) {
|
||||
buf.extend_from_slice(&self.to_be_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
impl KeySerialize for u16 {
|
||||
fn serialize(&self, buf: &mut Vec<u8>) {
|
||||
buf.extend_from_slice(&self.to_be_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
impl KeySerialize for u64 {
|
||||
fn serialize(&self, buf: &mut Vec<u8>) {
|
||||
buf.extend_from_slice(&self.to_be_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
impl DeserializeBigEndian for &[u8] {
|
||||
fn deserialize_be_u16(&self, index: usize) -> trc::Result<u16> {
|
||||
self.get(index..index + U16_LEN)
|
||||
.and_then(|bytes| bytes.try_into().ok())
|
||||
.ok_or_else(|| {
|
||||
trc::StoreEvent::DataCorruption
|
||||
.caused_by(trc::location!())
|
||||
.ctx(trc::Key::Value, *self)
|
||||
})
|
||||
.map(u16::from_be_bytes)
|
||||
}
|
||||
|
||||
fn deserialize_be_u32(&self, index: usize) -> trc::Result<u32> {
|
||||
self.get(index..index + U32_LEN)
|
||||
.and_then(|bytes| bytes.try_into().ok())
|
||||
.ok_or_else(|| {
|
||||
trc::StoreEvent::DataCorruption
|
||||
.caused_by(trc::location!())
|
||||
.ctx(trc::Key::Value, *self)
|
||||
})
|
||||
.map(u32::from_be_bytes)
|
||||
}
|
||||
|
||||
fn deserialize_be_u64(&self, index: usize) -> trc::Result<u64> {
|
||||
self.get(index..index + U64_LEN)
|
||||
.and_then(|bytes| bytes.try_into().ok())
|
||||
.ok_or_else(|| {
|
||||
trc::StoreEvent::DataCorruption
|
||||
.caused_by(trc::location!())
|
||||
.ctx(trc::Key::Value, *self)
|
||||
})
|
||||
.map(u64::from_be_bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<ValueClass>> ValueKey<T> {
|
||||
pub fn with_document_id(self, document_id: u32) -> Self {
|
||||
Self {
|
||||
document_id,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ValueKey<ValueClass> {
|
||||
pub fn property(
|
||||
account_id: u32,
|
||||
collection: impl Into<u8>,
|
||||
document_id: u32,
|
||||
field: impl Into<u8>,
|
||||
) -> ValueKey<ValueClass> {
|
||||
ValueKey {
|
||||
account_id,
|
||||
collection: collection.into(),
|
||||
document_id,
|
||||
class: ValueClass::Property(field.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn archive(
|
||||
account_id: u32,
|
||||
collection: impl Into<u8>,
|
||||
document_id: u32,
|
||||
) -> ValueKey<ValueClass> {
|
||||
ValueKey {
|
||||
account_id,
|
||||
collection: collection.into(),
|
||||
document_id,
|
||||
class: ValueClass::Property(Field::ARCHIVE.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Key for IndexKeyPrefix {
|
||||
fn serialize(&self, flags: u32) -> Vec<u8> {
|
||||
{
|
||||
if (flags & WITH_SUBSPACE) != 0 {
|
||||
KeySerializer::new(std::mem::size_of::<IndexKeyPrefix>() + 1)
|
||||
.write(crate::SUBSPACE_INDEXES)
|
||||
} else {
|
||||
KeySerializer::new(std::mem::size_of::<IndexKeyPrefix>())
|
||||
}
|
||||
}
|
||||
.write(self.account_id)
|
||||
.write(self.collection)
|
||||
.write(self.field)
|
||||
.finalize()
|
||||
}
|
||||
|
||||
fn subspace(&self) -> u8 {
|
||||
SUBSPACE_INDEXES
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexKeyPrefix {
|
||||
pub fn len() -> usize {
|
||||
U32_LEN + 2
|
||||
}
|
||||
}
|
||||
|
||||
impl Key for LogKey {
|
||||
fn subspace(&self) -> u8 {
|
||||
SUBSPACE_LOGS
|
||||
}
|
||||
|
||||
fn serialize(&self, flags: u32) -> Vec<u8> {
|
||||
{
|
||||
if (flags & WITH_SUBSPACE) != 0 {
|
||||
KeySerializer::new(std::mem::size_of::<LogKey>() + 1).write(crate::SUBSPACE_LOGS)
|
||||
} else {
|
||||
KeySerializer::new(std::mem::size_of::<LogKey>())
|
||||
}
|
||||
}
|
||||
.write(self.account_id)
|
||||
.write(self.collection)
|
||||
.write(self.change_id)
|
||||
.finalize()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<ValueClass> + Sync + Send + Clone> Key for ValueKey<T> {
|
||||
fn subspace(&self) -> u8 {
|
||||
self.class.as_ref().subspace(self.collection)
|
||||
}
|
||||
|
||||
fn serialize(&self, flags: u32) -> Vec<u8> {
|
||||
self.class
|
||||
.as_ref()
|
||||
.serialize(self.account_id, self.collection, self.document_id, flags)
|
||||
}
|
||||
}
|
||||
|
||||
impl ValueClass {
|
||||
pub fn serialize(
|
||||
&self,
|
||||
account_id: u32,
|
||||
collection: u8,
|
||||
document_id: u32,
|
||||
flags: u32,
|
||||
) -> Vec<u8> {
|
||||
let serializer = if (flags & WITH_SUBSPACE) != 0 {
|
||||
KeySerializer::new(self.serialized_size() + 2).write(self.subspace(collection))
|
||||
} else {
|
||||
KeySerializer::new(self.serialized_size() + 1)
|
||||
};
|
||||
|
||||
match self {
|
||||
ValueClass::Property(property) => serializer
|
||||
.write(account_id)
|
||||
.write(collection)
|
||||
.write(*property)
|
||||
.write(document_id),
|
||||
ValueClass::IndexProperty(property) => match property {
|
||||
IndexPropertyClass::Hash { property, hash } => serializer
|
||||
.write(account_id)
|
||||
.write(collection)
|
||||
.write(*property)
|
||||
.write(hash.as_bytes())
|
||||
.write(document_id),
|
||||
IndexPropertyClass::Integer { property, value } => serializer
|
||||
.write(account_id)
|
||||
.write(collection)
|
||||
.write(*property)
|
||||
.write(*value)
|
||||
.write(document_id),
|
||||
},
|
||||
ValueClass::Acl(grant_account_id) => serializer
|
||||
.write(*grant_account_id)
|
||||
.write(account_id)
|
||||
.write(collection)
|
||||
.write(document_id),
|
||||
ValueClass::TaskQueue(task) => match task {
|
||||
TaskQueueClass::Task { id } => serializer.write(0u64).write(*id),
|
||||
TaskQueueClass::Due { id, due } => serializer.write(*due).write(*id),
|
||||
},
|
||||
ValueClass::Blob(op) => match op {
|
||||
BlobOp::Commit { hash } => serializer.write::<&[u8]>(hash.as_ref()),
|
||||
BlobOp::Link { hash, to } => match to {
|
||||
BlobLink::Id { id } => serializer.write::<&[u8]>(hash.as_ref()).write(*id),
|
||||
BlobLink::Document => serializer
|
||||
.write::<&[u8]>(hash.as_ref())
|
||||
.write(account_id)
|
||||
.write(collection)
|
||||
.write(document_id),
|
||||
BlobLink::Temporary { until } => serializer
|
||||
.write::<&[u8]>(hash.as_ref())
|
||||
.write(account_id)
|
||||
.write(*until),
|
||||
},
|
||||
},
|
||||
ValueClass::InMemory(lookup) => match lookup {
|
||||
InMemoryClass::Key(key) => serializer.write(key.as_slice()),
|
||||
InMemoryClass::Counter(key) => serializer.write(key.as_slice()),
|
||||
},
|
||||
ValueClass::Registry(registry) => match registry {
|
||||
RegistryClass::Item { object_id, item_id } => {
|
||||
serializer.write(*object_id).write(*item_id)
|
||||
}
|
||||
RegistryClass::IndexId { object_id, item_id } => {
|
||||
serializer.write(u16::MAX).write(*object_id).write(*item_id)
|
||||
}
|
||||
RegistryClass::Index {
|
||||
index_id,
|
||||
object_id,
|
||||
item_id,
|
||||
key,
|
||||
} => serializer
|
||||
.write(*object_id)
|
||||
.write(*index_id)
|
||||
.write(key.as_slice())
|
||||
.write(*item_id),
|
||||
RegistryClass::Reference {
|
||||
to_object_id,
|
||||
to_item_id,
|
||||
from_object_id,
|
||||
from_item_id,
|
||||
} => serializer
|
||||
.write(*to_object_id)
|
||||
.write(*to_item_id)
|
||||
.write(*from_object_id)
|
||||
.write(*from_item_id),
|
||||
RegistryClass::PrimaryKey {
|
||||
object_id,
|
||||
index_id,
|
||||
key,
|
||||
} => serializer
|
||||
.write((*object_id).unwrap_or(u16::MAX))
|
||||
.write(*index_id)
|
||||
.write(key.as_slice()),
|
||||
RegistryClass::IdCounter { object_id } => serializer.write(*object_id),
|
||||
},
|
||||
ValueClass::Queue(queue) => match queue {
|
||||
QueueClass::Message(queue_id) => serializer.write(*queue_id),
|
||||
QueueClass::MessageEvent(event) => serializer
|
||||
.write(event.due)
|
||||
.write(event.queue_id)
|
||||
.write(event.queue_name.as_slice()),
|
||||
QueueClass::QuotaCount(key) => serializer.write(0u8).write(key.as_slice()),
|
||||
QueueClass::QuotaSize(key) => serializer.write(1u8).write(key.as_slice()),
|
||||
},
|
||||
ValueClass::Telemetry(telemetry) => match telemetry {
|
||||
TelemetryClass::Span(span_id) => serializer.write(*span_id),
|
||||
TelemetryClass::Metric(metric_id) => serializer.write(*metric_id),
|
||||
},
|
||||
ValueClass::DocumentId => serializer.write(account_id).write(collection),
|
||||
ValueClass::ChangeId => serializer.write(account_id),
|
||||
ValueClass::Quota => serializer.write(account_id).write(u8::MAX),
|
||||
ValueClass::TenantQuota(tenant_id) => serializer.write(*tenant_id).write(u8::MAX - 1),
|
||||
ValueClass::NodeId(node_id) => serializer.write(u32::MAX).write(*node_id),
|
||||
ValueClass::ShareNotification {
|
||||
notification_id,
|
||||
notify_account_id,
|
||||
} => serializer
|
||||
.write(*notify_account_id)
|
||||
.write(u8::from(SyncCollection::ShareNotification))
|
||||
.write(*notification_id),
|
||||
ValueClass::SearchIndex(index) => match &index.typ {
|
||||
SearchIndexType::Term { field, hash } => {
|
||||
let class = index.index.as_u8();
|
||||
match &index.id {
|
||||
SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id,
|
||||
} => serializer
|
||||
.write(class)
|
||||
.write(*account_id)
|
||||
.write(hash.payload())
|
||||
.write(hash.payload_len())
|
||||
.write(*field)
|
||||
.write(*document_id),
|
||||
SearchIndexId::Global { id } => serializer
|
||||
.write(class)
|
||||
.write(hash.payload())
|
||||
.write(hash.payload_len())
|
||||
.write(*field)
|
||||
.write(*id),
|
||||
}
|
||||
}
|
||||
SearchIndexType::Index { field } => {
|
||||
let class = index.index.as_u8() | 1 << 6;
|
||||
match &index.id {
|
||||
SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id,
|
||||
} => serializer
|
||||
.write(class)
|
||||
.write(*account_id)
|
||||
.write(field.field_id)
|
||||
.write(field.data.as_slice())
|
||||
.write(*document_id),
|
||||
SearchIndexId::Global { id } => serializer
|
||||
.write(class)
|
||||
.write(field.field_id)
|
||||
.write(field.data.as_slice())
|
||||
.write(*id),
|
||||
}
|
||||
}
|
||||
SearchIndexType::Document => {
|
||||
let class = index.index.as_u8() | 2 << 6;
|
||||
match &index.id {
|
||||
SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id,
|
||||
} => serializer
|
||||
.write(class)
|
||||
.write(*account_id)
|
||||
.write(*document_id),
|
||||
SearchIndexId::Global { id } => serializer.write(class).write(*id),
|
||||
}
|
||||
}
|
||||
},
|
||||
ValueClass::Any(any) => serializer.write(any.key.as_slice()),
|
||||
}
|
||||
.finalize()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]> + Sync + Send + Clone> Key for IndexKey<T> {
|
||||
fn subspace(&self) -> u8 {
|
||||
SUBSPACE_INDEXES
|
||||
}
|
||||
|
||||
fn serialize(&self, flags: u32) -> Vec<u8> {
|
||||
let key = self.key.as_ref();
|
||||
{
|
||||
if (flags & WITH_SUBSPACE) != 0 {
|
||||
KeySerializer::new(std::mem::size_of::<IndexKey<T>>() + key.len() + 1)
|
||||
.write(crate::SUBSPACE_INDEXES)
|
||||
} else {
|
||||
KeySerializer::new(std::mem::size_of::<IndexKey<T>>() + key.len())
|
||||
}
|
||||
}
|
||||
.write(self.account_id)
|
||||
.write(self.collection)
|
||||
.write(self.field)
|
||||
.write(key)
|
||||
.write(self.document_id)
|
||||
.finalize()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]> + Sync + Send + Clone> Key for AnyKey<T> {
|
||||
fn serialize(&self, flags: u32) -> Vec<u8> {
|
||||
let key = self.key.as_ref();
|
||||
if (flags & WITH_SUBSPACE) != 0 {
|
||||
KeySerializer::new(key.len() + 1).write(self.subspace)
|
||||
} else {
|
||||
KeySerializer::new(key.len())
|
||||
}
|
||||
.write(key)
|
||||
.finalize()
|
||||
}
|
||||
|
||||
fn subspace(&self) -> u8 {
|
||||
self.subspace
|
||||
}
|
||||
}
|
||||
|
||||
const MAILBOX_COLLECTION: u8 = Collection::Mailbox as u8;
|
||||
const MAILBOX_COUNTER_FIELD: u8 = MailboxField::UidCounter as u8;
|
||||
const REG_ARCHIVED_ITEM: u16 = ObjectType::ArchivedItem as u16;
|
||||
const REG_SPAM_SAMPLE: u16 = ObjectType::SpamTrainingSample as u16;
|
||||
const REG_ACCOUNT: u16 = ObjectType::Account as u16;
|
||||
const REG_DOMAIN: u16 = ObjectType::Domain as u16;
|
||||
const REG_TENANT: u16 = ObjectType::Tenant as u16;
|
||||
const REG_ROLE: u16 = ObjectType::Role as u16;
|
||||
const REG_OAUTH_CLIENT: u16 = ObjectType::OAuthClient as u16;
|
||||
const REG_MAILING_LIST: u16 = ObjectType::MailingList as u16;
|
||||
const REG_MASKED_EMAIL: u16 = ObjectType::MaskedEmail as u16;
|
||||
const REG_PUBLIC_KEY: u16 = ObjectType::PublicKey as u16;
|
||||
const REG_TRACE: u16 = ObjectType::Trace as u16;
|
||||
const REG_METRIC: u16 = ObjectType::Metric as u16;
|
||||
const REPORT_EXTERNAL_ARF: u16 = ObjectType::ArfExternalReport as u16;
|
||||
const REPORT_EXTERNAL_DMARC: u16 = ObjectType::DmarcExternalReport as u16;
|
||||
const REPORT_EXTERNAL_TLS: u16 = ObjectType::TlsExternalReport as u16;
|
||||
const REPORT_INTERNAL_DMARC: u16 = ObjectType::DmarcInternalReport as u16;
|
||||
const REPORT_INTERNAL_TLS: u16 = ObjectType::TlsInternalReport as u16;
|
||||
|
||||
impl ValueClass {
|
||||
pub fn serialized_size(&self) -> usize {
|
||||
match self {
|
||||
ValueClass::Property(_) => U32_LEN * 2 + 3,
|
||||
ValueClass::IndexProperty(p) => match p {
|
||||
IndexPropertyClass::Hash { hash, .. } => U32_LEN * 2 + 3 + hash.len(),
|
||||
IndexPropertyClass::Integer { .. } => U32_LEN * 2 + 3 + U64_LEN,
|
||||
},
|
||||
ValueClass::Acl(_) => U32_LEN * 3 + 2,
|
||||
ValueClass::InMemory(InMemoryClass::Counter(v) | InMemoryClass::Key(v)) => v.len(),
|
||||
ValueClass::Registry(registry) => match registry {
|
||||
RegistryClass::Item { .. } => U16_LEN + U64_LEN + 1,
|
||||
RegistryClass::Reference { .. } => ((U16_LEN + U64_LEN) * 2) + 1,
|
||||
RegistryClass::Index { key, .. } => (U16_LEN * 2) + U64_LEN + key.len() + 1,
|
||||
RegistryClass::PrimaryKey { key, .. } => (U16_LEN * 2) + key.len() + 1,
|
||||
RegistryClass::IndexId { .. } => U16_LEN + U64_LEN + 1,
|
||||
RegistryClass::IdCounter { .. } => U16_LEN + 1,
|
||||
},
|
||||
ValueClass::Blob(op) => match op {
|
||||
BlobOp::Commit { .. } => BLOB_HASH_LEN,
|
||||
BlobOp::Link { to, .. } => {
|
||||
BLOB_HASH_LEN
|
||||
+ match to {
|
||||
BlobLink::Id { .. } => U64_LEN,
|
||||
BlobLink::Document => U32_LEN * 2 + 1,
|
||||
BlobLink::Temporary { .. } => U32_LEN + U64_LEN,
|
||||
}
|
||||
}
|
||||
},
|
||||
ValueClass::TaskQueue(_) => (U64_LEN * 2) + 1,
|
||||
ValueClass::Queue(q) => match q {
|
||||
QueueClass::Message(_) => U64_LEN,
|
||||
QueueClass::MessageEvent(_) => U64_LEN * 3,
|
||||
QueueClass::QuotaCount(v) | QueueClass::QuotaSize(v) => v.len(),
|
||||
},
|
||||
ValueClass::Telemetry(telemetry) => match telemetry {
|
||||
TelemetryClass::Span(_) | TelemetryClass::Metric(_) => U64_LEN + 1,
|
||||
},
|
||||
ValueClass::DocumentId | ValueClass::Quota | ValueClass::TenantQuota(_) => U32_LEN + 1,
|
||||
ValueClass::ChangeId => U32_LEN,
|
||||
ValueClass::ShareNotification { .. } => U32_LEN + U64_LEN + 1,
|
||||
ValueClass::NodeId(_) => (U16_LEN * 3) + 1,
|
||||
ValueClass::SearchIndex(v) => match &v.typ {
|
||||
SearchIndexType::Term { hash, .. } => U64_LEN + hash.len() + 2,
|
||||
SearchIndexType::Index { field, .. } => 1 + field.data.len() + U64_LEN,
|
||||
SearchIndexType::Document => match &v.id {
|
||||
SearchIndexId::Account { .. } => 1 + U32_LEN * 2,
|
||||
SearchIndexId::Global { .. } => 1 + U64_LEN,
|
||||
},
|
||||
},
|
||||
ValueClass::Any(v) => v.key.len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subspace(&self, collection: u8) -> u8 {
|
||||
match self {
|
||||
ValueClass::Property(field) => {
|
||||
if collection == MAILBOX_COLLECTION && *field == MAILBOX_COUNTER_FIELD {
|
||||
SUBSPACE_COUNTER
|
||||
} else {
|
||||
SUBSPACE_PROPERTY
|
||||
}
|
||||
}
|
||||
ValueClass::IndexProperty { .. } => SUBSPACE_PROPERTY,
|
||||
ValueClass::Acl(_) => SUBSPACE_ACL,
|
||||
ValueClass::TaskQueue { .. } => SUBSPACE_TASK_QUEUE,
|
||||
ValueClass::Blob(op) => match op {
|
||||
BlobOp::Commit { .. } | BlobOp::Link { .. } => SUBSPACE_BLOB_LINK,
|
||||
},
|
||||
ValueClass::Registry(registry) => match registry {
|
||||
RegistryClass::Item { object_id, .. } => match *object_id {
|
||||
REG_ACCOUNT | REG_DOMAIN | REG_TENANT | REG_ROLE | REG_OAUTH_CLIENT
|
||||
| REG_MAILING_LIST | REG_MASKED_EMAIL | REG_PUBLIC_KEY => SUBSPACE_DIRECTORY,
|
||||
REG_ARCHIVED_ITEM => SUBSPACE_DELETED_ITEMS,
|
||||
REG_SPAM_SAMPLE => SUBSPACE_SPAM_SAMPLES,
|
||||
REG_TRACE => SUBSPACE_TELEMETRY_SPAN,
|
||||
REG_METRIC => SUBSPACE_TELEMETRY_METRIC,
|
||||
REPORT_EXTERNAL_ARF | REPORT_EXTERNAL_DMARC | REPORT_EXTERNAL_TLS => {
|
||||
SUBSPACE_REPORT_IN
|
||||
}
|
||||
REPORT_INTERNAL_DMARC | REPORT_INTERNAL_TLS => SUBSPACE_REPORT_OUT,
|
||||
_ => SUBSPACE_REGISTRY,
|
||||
},
|
||||
RegistryClass::IndexId { .. } | RegistryClass::Index { .. } => {
|
||||
SUBSPACE_REGISTRY_IDX
|
||||
}
|
||||
RegistryClass::Reference { .. } | RegistryClass::PrimaryKey { .. } => {
|
||||
SUBSPACE_REGISTRY_PK
|
||||
}
|
||||
RegistryClass::IdCounter { .. } => SUBSPACE_COUNTER,
|
||||
},
|
||||
ValueClass::NodeId(_) => SUBSPACE_REGISTRY_PK,
|
||||
ValueClass::InMemory(lookup) => match lookup {
|
||||
InMemoryClass::Key(_) => SUBSPACE_IN_MEMORY_VALUE,
|
||||
InMemoryClass::Counter(_) => SUBSPACE_IN_MEMORY_COUNTER,
|
||||
},
|
||||
ValueClass::Queue(queue) => match queue {
|
||||
QueueClass::Message(_) => SUBSPACE_QUEUE_MESSAGE,
|
||||
QueueClass::MessageEvent(_) => SUBSPACE_QUEUE_EVENT,
|
||||
QueueClass::QuotaCount(_) | QueueClass::QuotaSize(_) => SUBSPACE_QUOTA,
|
||||
},
|
||||
ValueClass::Telemetry(telemetry) => match telemetry {
|
||||
TelemetryClass::Span { .. } => SUBSPACE_TELEMETRY_SPAN,
|
||||
TelemetryClass::Metric { .. } => SUBSPACE_TELEMETRY_METRIC,
|
||||
},
|
||||
ValueClass::DocumentId
|
||||
| ValueClass::ChangeId
|
||||
| ValueClass::Quota
|
||||
| ValueClass::TenantQuota(_) => SUBSPACE_COUNTER,
|
||||
ValueClass::ShareNotification { .. } => SUBSPACE_LOGS,
|
||||
ValueClass::SearchIndex(_) => SUBSPACE_SEARCH_INDEX,
|
||||
ValueClass::Any(any) => any.subspace,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_node_id_key(key: &[u8]) -> bool {
|
||||
key.len() == U32_LEN + U16_LEN && key.starts_with(&u32::MAX.to_be_bytes())
|
||||
}
|
||||
|
||||
impl From<ValueClass> for ValueKey<ValueClass> {
|
||||
fn from(class: ValueClass) -> Self {
|
||||
ValueKey {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
class,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RegistryClass> for ValueKey<ValueClass> {
|
||||
fn from(value: RegistryClass) -> Self {
|
||||
ValueKey {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
class: ValueClass::Registry(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RegistryClass> for ValueClass {
|
||||
fn from(value: RegistryClass) -> Self {
|
||||
ValueClass::Registry(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlobOp> for ValueClass {
|
||||
fn from(value: BlobOp) -> Self {
|
||||
ValueClass::Blob(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchIndex {
|
||||
pub fn to_u8(&self) -> u8 {
|
||||
match self {
|
||||
SearchIndex::Email => 0,
|
||||
SearchIndex::Calendar => 1,
|
||||
SearchIndex::Contacts => 2,
|
||||
SearchIndex::File => 3,
|
||||
SearchIndex::Tracing => 4,
|
||||
SearchIndex::InMemory => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_u8(value: u8) -> Option<Self> {
|
||||
match value {
|
||||
0 => Some(SearchIndex::Email),
|
||||
1 => Some(SearchIndex::Calendar),
|
||||
2 => Some(SearchIndex::Contacts),
|
||||
3 => Some(SearchIndex::File),
|
||||
4 => Some(SearchIndex::Tracing),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
SearchIndex::Email => "email",
|
||||
SearchIndex::Calendar => "calendar",
|
||||
SearchIndex::Contacts => "contacts",
|
||||
SearchIndex::File => "file",
|
||||
SearchIndex::Tracing => "tracing",
|
||||
SearchIndex::InMemory => "in_memory",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_str(value: &str) -> Option<Self> {
|
||||
match value {
|
||||
"email" => Some(SearchIndex::Email),
|
||||
"calendar" => Some(SearchIndex::Calendar),
|
||||
"contacts" => Some(SearchIndex::Contacts),
|
||||
"file" => Some(SearchIndex::File),
|
||||
"tracing" => Some(SearchIndex::Tracing),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{SerializeInfallible, U64_LEN};
|
||||
use ahash::AHashSet;
|
||||
use types::collection::{SyncCollection, VanishedCollection};
|
||||
use utils::{codec::leb128::Leb128Vec, map::vec_map::VecMap};
|
||||
|
||||
use super::key::KeySerializer;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub(crate) struct ChangeLogBuilder {
|
||||
pub changes: VecMap<SyncCollection, Changes>,
|
||||
pub vanished: VecMap<VanishedCollection, VanishedItems>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum VanishedItem {
|
||||
Name(String),
|
||||
Id(u64),
|
||||
IdPair(u32, u32),
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub(crate) struct VanishedItems(Vec<VanishedItem>);
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Changes {
|
||||
pub item_inserts: AHashSet<u64>,
|
||||
pub item_updates: AHashSet<u64>,
|
||||
pub item_deletes: AHashSet<u64>,
|
||||
|
||||
pub container_inserts: AHashSet<u32>,
|
||||
pub container_updates: AHashSet<u32>,
|
||||
pub container_deletes: AHashSet<u32>,
|
||||
pub container_property_changes: AHashSet<u32>,
|
||||
}
|
||||
|
||||
impl ChangeLogBuilder {
|
||||
pub fn log_container_insert(&mut self, collection: SyncCollection, document_id: u32) {
|
||||
let changes = self.changes.get_mut_or_insert(collection);
|
||||
if changes.container_deletes.remove(&document_id) {
|
||||
changes.container_updates.insert(document_id);
|
||||
} else {
|
||||
changes.container_inserts.insert(document_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_item_insert(
|
||||
&mut self,
|
||||
collection: SyncCollection,
|
||||
prefix: Option<u32>,
|
||||
document_id: u32,
|
||||
) {
|
||||
let id = build_id(prefix, document_id);
|
||||
let changes = self.changes.get_mut_or_insert(collection);
|
||||
if changes.item_deletes.remove(&id) {
|
||||
changes.item_updates.insert(id);
|
||||
} else {
|
||||
changes.item_inserts.insert(id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_container_update(&mut self, collection: SyncCollection, document_id: u32) {
|
||||
self.changes
|
||||
.get_mut_or_insert(collection)
|
||||
.container_updates
|
||||
.insert(document_id);
|
||||
}
|
||||
|
||||
pub fn log_container_property_update(&mut self, collection: SyncCollection, document_id: u32) {
|
||||
self.changes
|
||||
.get_mut_or_insert(collection)
|
||||
.container_property_changes
|
||||
.insert(document_id);
|
||||
}
|
||||
|
||||
pub fn log_item_update(
|
||||
&mut self,
|
||||
collection: SyncCollection,
|
||||
prefix: Option<u32>,
|
||||
document_id: u32,
|
||||
) {
|
||||
self.changes
|
||||
.get_mut_or_insert(collection)
|
||||
.item_updates
|
||||
.insert(build_id(prefix, document_id));
|
||||
}
|
||||
|
||||
pub fn log_container_delete(&mut self, collection: SyncCollection, document_id: u32) {
|
||||
let changes = self.changes.get_mut_or_insert(collection);
|
||||
let id = document_id;
|
||||
changes.container_updates.remove(&id);
|
||||
changes.container_property_changes.remove(&id);
|
||||
changes.container_deletes.insert(id);
|
||||
}
|
||||
|
||||
pub fn log_item_delete(
|
||||
&mut self,
|
||||
collection: SyncCollection,
|
||||
prefix: Option<u32>,
|
||||
document_id: u32,
|
||||
) {
|
||||
let changes = self.changes.get_mut_or_insert(collection);
|
||||
let id = build_id(prefix, document_id);
|
||||
changes.item_updates.remove(&id);
|
||||
changes.item_deletes.insert(id);
|
||||
}
|
||||
|
||||
pub fn log_vanished_item(
|
||||
&mut self,
|
||||
collection: VanishedCollection,
|
||||
item: impl Into<VanishedItem>,
|
||||
) {
|
||||
self.vanished
|
||||
.get_mut_or_insert(collection)
|
||||
.0
|
||||
.push(item.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn build_id(prefix: Option<u32>, document_id: u32) -> u64 {
|
||||
if let Some(prefix) = prefix {
|
||||
((prefix as u64) << 32) | document_id as u64
|
||||
} else {
|
||||
document_id as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl Changes {
|
||||
pub fn has_container_changes(&self) -> bool {
|
||||
!self.container_inserts.is_empty()
|
||||
|| !self.container_updates.is_empty()
|
||||
|| !self.container_property_changes.is_empty()
|
||||
|| !self.container_deletes.is_empty()
|
||||
}
|
||||
|
||||
pub fn has_item_changes(&self) -> bool {
|
||||
!self.item_inserts.is_empty()
|
||||
|| !self.item_updates.is_empty()
|
||||
|| !self.item_deletes.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializeInfallible for Changes {
|
||||
fn serialize(&self) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(
|
||||
1 + (self.item_inserts.len()
|
||||
+ self.item_updates.len()
|
||||
+ self.item_deletes.len()
|
||||
+ self.container_inserts.len()
|
||||
+ self.container_updates.len()
|
||||
+ self.container_property_changes.len()
|
||||
+ self.container_deletes.len()
|
||||
+ 4)
|
||||
* std::mem::size_of::<usize>(),
|
||||
);
|
||||
|
||||
buf.push_leb128(self.container_inserts.len());
|
||||
buf.push_leb128(self.container_updates.len());
|
||||
buf.push_leb128(self.container_property_changes.len());
|
||||
buf.push_leb128(self.container_deletes.len());
|
||||
buf.push_leb128(self.item_inserts.len());
|
||||
buf.push_leb128(self.item_updates.len());
|
||||
buf.push_leb128(self.item_deletes.len());
|
||||
|
||||
for list in [
|
||||
&self.container_inserts,
|
||||
&self.container_updates,
|
||||
&self.container_property_changes,
|
||||
&self.container_deletes,
|
||||
] {
|
||||
for id in list {
|
||||
buf.push_leb128(*id);
|
||||
}
|
||||
}
|
||||
for list in [&self.item_inserts, &self.item_updates, &self.item_deletes] {
|
||||
for id in list {
|
||||
buf.push_leb128(*id);
|
||||
}
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for VanishedItem {
|
||||
fn from(value: String) -> Self {
|
||||
VanishedItem::Name(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for VanishedItem {
|
||||
fn from(value: u64) -> Self {
|
||||
VanishedItem::Id(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(u32, u32)> for VanishedItem {
|
||||
fn from(value: (u32, u32)) -> Self {
|
||||
VanishedItem::Id((value.0 as u64) << 32 | value.1 as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl VanishedItem {
|
||||
pub fn serialized_size(&self) -> usize {
|
||||
match self {
|
||||
VanishedItem::Name(name) => name.len() + 1,
|
||||
VanishedItem::Id(_) | VanishedItem::IdPair(..) => U64_LEN,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializeInfallible for VanishedItems {
|
||||
fn serialize(&self) -> Vec<u8> {
|
||||
let mut buf = KeySerializer::new(64);
|
||||
|
||||
for item in &self.0 {
|
||||
buf = match item {
|
||||
VanishedItem::Name(name) => buf.write(name.as_bytes()).write(0u8),
|
||||
VanishedItem::Id(id) => buf.write(id.to_be_bytes().as_slice()),
|
||||
VanishedItem::IdPair(a, b) => buf
|
||||
.write(a.to_be_bytes().as_slice())
|
||||
.write(b.to_be_bytes().as_slice()),
|
||||
};
|
||||
}
|
||||
|
||||
buf.finalize()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,700 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use self::assert::AssertValue;
|
||||
use crate::backend::MAX_TOKEN_LENGTH;
|
||||
use log::ChangeLogBuilder;
|
||||
use nlp::tokenizers::word::WordTokenizer;
|
||||
use rkyv::util::AlignedVec;
|
||||
use std::{collections::HashSet, hash::Hash, time::SystemTime};
|
||||
use types::{
|
||||
blob_hash::BlobHash,
|
||||
collection::{Collection, SyncCollection, VanishedCollection},
|
||||
field::{
|
||||
CalendarEventField, CalendarNotificationField, ContactField, EmailField,
|
||||
EmailSubmissionField, Field, MailboxField, PrincipalField, SieveField,
|
||||
},
|
||||
};
|
||||
use utils::{
|
||||
cheeky_hash::CheekyHash,
|
||||
map::{bitmap::Bitmap, vec_map::VecMap},
|
||||
};
|
||||
|
||||
pub mod assert;
|
||||
pub mod batch;
|
||||
pub mod bitpack;
|
||||
pub mod blob;
|
||||
pub mod key;
|
||||
pub mod log;
|
||||
pub mod serialize;
|
||||
|
||||
pub(crate) const ARCHIVE_ALIGNMENT: usize = 16;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Archive<T> {
|
||||
pub inner: T,
|
||||
pub version: ArchiveVersion,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum ArchiveVersion {
|
||||
Versioned { change_id: u64, hash: u32 },
|
||||
Hashed { hash: u32 },
|
||||
Unversioned,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AlignedBytes {
|
||||
Aligned(AlignedVec<ARCHIVE_ALIGNMENT>),
|
||||
Vec(Vec<u8>),
|
||||
}
|
||||
|
||||
pub struct Archiver<T>
|
||||
where
|
||||
T: rkyv::Archive
|
||||
+ for<'a> rkyv::Serialize<
|
||||
rkyv::api::high::HighSerializer<
|
||||
rkyv::util::AlignedVec,
|
||||
rkyv::ser::allocator::ArenaHandle<'a>,
|
||||
rkyv::rancor::Error,
|
||||
>,
|
||||
>,
|
||||
{
|
||||
pub inner: T,
|
||||
pub flags: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AssignedIds {
|
||||
pub ids: Vec<AssignedId>,
|
||||
current_change_id: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AssignedId {
|
||||
Counter(i64),
|
||||
ChangeId(ChangeId),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ChangeId {
|
||||
pub account_id: u32,
|
||||
pub change_id: u64,
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
feature = "rocks",
|
||||
feature = "postgres",
|
||||
feature = "mysql",
|
||||
feature = "foundation"
|
||||
))]
|
||||
pub(crate) use commit_limits::{MAX_COMMIT_ATTEMPTS, MAX_COMMIT_TIME};
|
||||
|
||||
#[cfg(any(
|
||||
feature = "rocks",
|
||||
feature = "postgres",
|
||||
feature = "mysql",
|
||||
feature = "foundation"
|
||||
))]
|
||||
mod commit_limits {
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
pub(crate) const MAX_COMMIT_ATTEMPTS: u32 = 10;
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
pub(crate) const MAX_COMMIT_TIME: Duration = Duration::from_secs(10);
|
||||
|
||||
#[cfg(feature = "test_mode")]
|
||||
pub(crate) const MAX_COMMIT_ATTEMPTS: u32 = 1000;
|
||||
#[cfg(feature = "test_mode")]
|
||||
pub(crate) const MAX_COMMIT_TIME: Duration = Duration::from_secs(3600);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Batch<'x> {
|
||||
pub(crate) changes: &'x VecMap<u32, ChangedCollection>,
|
||||
pub(crate) ops: &'x mut [Operation],
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BatchBuilder {
|
||||
current_account_id: Option<u32>,
|
||||
current_collection: Option<Collection>,
|
||||
current_document_id: Option<u32>,
|
||||
changes: VecMap<u32, ChangeLogBuilder>,
|
||||
changed_collections: VecMap<u32, ChangedCollection>,
|
||||
has_assertions: bool,
|
||||
batch_size: usize,
|
||||
batch_ops: usize,
|
||||
commit_points: Vec<usize>,
|
||||
ops: Vec<Operation>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ChangedCollection {
|
||||
pub changed_containers: Bitmap<SyncCollection>,
|
||||
pub changed_items: Bitmap<SyncCollection>,
|
||||
pub share_notification_id: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Operation {
|
||||
AccountId {
|
||||
account_id: u32,
|
||||
},
|
||||
Collection {
|
||||
collection: Collection,
|
||||
},
|
||||
DocumentId {
|
||||
document_id: u32,
|
||||
},
|
||||
AssertValue {
|
||||
class: ValueClass,
|
||||
assert_value: AssertValue,
|
||||
},
|
||||
Value {
|
||||
class: ValueClass,
|
||||
op: ValueOp,
|
||||
},
|
||||
Index {
|
||||
field: u8,
|
||||
key: Vec<u8>,
|
||||
set: bool,
|
||||
},
|
||||
Log {
|
||||
collection: LogCollection,
|
||||
set: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
|
||||
pub enum LogCollection {
|
||||
Sync(SyncCollection),
|
||||
Vanished(VanishedCollection),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Eq, Hash)]
|
||||
pub enum ValueClass {
|
||||
Property(u8),
|
||||
IndexProperty(IndexPropertyClass),
|
||||
Acl(u32),
|
||||
InMemory(InMemoryClass),
|
||||
TaskQueue(TaskQueueClass),
|
||||
Blob(BlobOp),
|
||||
Registry(RegistryClass),
|
||||
Queue(QueueClass),
|
||||
Telemetry(TelemetryClass),
|
||||
SearchIndex(SearchIndexClass),
|
||||
Any(AnyClass),
|
||||
ShareNotification {
|
||||
notification_id: u64,
|
||||
notify_account_id: u32,
|
||||
},
|
||||
DocumentId,
|
||||
ChangeId,
|
||||
Quota,
|
||||
TenantQuota(u32),
|
||||
NodeId(u16),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Eq, Hash)]
|
||||
pub enum IndexPropertyClass {
|
||||
Hash { property: u8, hash: CheekyHash },
|
||||
Integer { property: u8, value: u64 },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Eq, Hash)]
|
||||
pub struct SearchIndexClass {
|
||||
pub index: SearchIndex,
|
||||
pub id: SearchIndexId,
|
||||
pub typ: SearchIndexType,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Eq, Hash)]
|
||||
pub enum SearchIndexType {
|
||||
Term { field: u8, hash: CheekyHash },
|
||||
Index { field: SearchIndexField },
|
||||
Document,
|
||||
}
|
||||
|
||||
pub(crate) const SEARCH_INDEX_MAX_FIELD_LEN: usize = 128;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)]
|
||||
pub struct SearchIndexField {
|
||||
pub(crate) field_id: u8,
|
||||
pub(crate) data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Eq, Hash)]
|
||||
pub enum SearchIndexId {
|
||||
Account { account_id: u32, document_id: u32 },
|
||||
Global { id: u64 },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Eq, Hash)]
|
||||
pub enum TaskQueueClass {
|
||||
Task { id: u64 },
|
||||
Due { id: u64, due: u64 },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Eq, Hash)]
|
||||
pub enum SearchIndex {
|
||||
Email,
|
||||
Calendar,
|
||||
Contacts,
|
||||
File,
|
||||
Tracing,
|
||||
InMemory,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Eq, Hash)]
|
||||
pub struct AnyClass {
|
||||
pub subspace: u8,
|
||||
pub key: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Eq, Hash)]
|
||||
pub enum InMemoryClass {
|
||||
Key(Vec<u8>),
|
||||
Counter(Vec<u8>),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Eq, Hash)]
|
||||
pub enum RegistryClass {
|
||||
Item {
|
||||
object_id: u16,
|
||||
item_id: u64,
|
||||
},
|
||||
Reference {
|
||||
to_object_id: u16,
|
||||
to_item_id: u64,
|
||||
from_object_id: u16,
|
||||
from_item_id: u64,
|
||||
},
|
||||
Index {
|
||||
index_id: u16,
|
||||
object_id: u16,
|
||||
item_id: u64,
|
||||
key: Vec<u8>,
|
||||
},
|
||||
IndexId {
|
||||
object_id: u16,
|
||||
item_id: u64,
|
||||
},
|
||||
PrimaryKey {
|
||||
object_id: Option<u16>,
|
||||
index_id: u16,
|
||||
key: Vec<u8>,
|
||||
},
|
||||
IdCounter {
|
||||
object_id: u16,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Eq, Hash)]
|
||||
pub enum QueueClass {
|
||||
Message(u64),
|
||||
MessageEvent(QueueEvent),
|
||||
QuotaCount(Vec<u8>),
|
||||
QuotaSize(Vec<u8>),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Eq, Hash)]
|
||||
pub enum TelemetryClass {
|
||||
Span(u64),
|
||||
Metric(u64),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Eq, Hash)]
|
||||
pub struct QueueEvent {
|
||||
pub due: u64,
|
||||
pub queue_id: u64,
|
||||
pub queue_name: [u8; 8],
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Default)]
|
||||
pub enum ValueOp {
|
||||
Set(Vec<u8>),
|
||||
SetFnc(SetOperation),
|
||||
MergeFnc(MergeOperation),
|
||||
AtomicAdd(i64),
|
||||
AddAndGet(i64),
|
||||
#[default]
|
||||
Clear,
|
||||
}
|
||||
|
||||
pub enum MergeResult {
|
||||
Update(Vec<u8>),
|
||||
Skip,
|
||||
Delete,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Param {
|
||||
I64(i64),
|
||||
U64(u64),
|
||||
String(String),
|
||||
Bytes(Vec<u8>),
|
||||
Bool(bool),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[repr(transparent)]
|
||||
pub struct Params(Vec<Param>);
|
||||
|
||||
pub type SetFnc = fn(&Params, &AssignedIds) -> trc::Result<Vec<u8>>;
|
||||
pub type MergeFnc = fn(&Params, &AssignedIds, Option<&[u8]>) -> trc::Result<MergeResult>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MergeOperation {
|
||||
pub(crate) fnc: MergeFnc,
|
||||
pub(crate) params: Params,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SetOperation {
|
||||
pub(crate) fnc: SetFnc,
|
||||
pub(crate) params: Params,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Eq, Hash)]
|
||||
pub enum BlobOp {
|
||||
Commit { hash: BlobHash },
|
||||
Link { hash: BlobHash, to: BlobLink },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Eq, Hash)]
|
||||
pub enum BlobLink {
|
||||
Id { id: u64 },
|
||||
Document,
|
||||
Temporary { until: u64 },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Eq, Hash)]
|
||||
pub struct AnyKey<T: AsRef<[u8]>> {
|
||||
pub subspace: u8,
|
||||
pub key: T,
|
||||
}
|
||||
|
||||
pub trait TokenizeText {
|
||||
fn tokenize_into(&self, tokens: &mut HashSet<String>);
|
||||
fn to_tokens(&self) -> HashSet<String>;
|
||||
}
|
||||
|
||||
impl TokenizeText for &str {
|
||||
fn tokenize_into(&self, tokens: &mut HashSet<String>) {
|
||||
for token in WordTokenizer::new(self, MAX_TOKEN_LENGTH) {
|
||||
tokens.insert(token.word.into_owned());
|
||||
}
|
||||
}
|
||||
|
||||
fn to_tokens(&self) -> HashSet<String> {
|
||||
let mut tokens = HashSet::new();
|
||||
self.tokenize_into(&mut tokens);
|
||||
tokens
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoOperations {
|
||||
fn build(self, batch: &mut BatchBuilder) -> trc::Result<()>;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn now() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map_or(0, |d| d.as_secs())
|
||||
}
|
||||
|
||||
impl AsRef<ValueClass> for ValueClass {
|
||||
fn as_ref(&self) -> &ValueClass {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AssignedIds {
|
||||
pub fn push_counter_id(&mut self, id: i64) {
|
||||
self.ids.push(AssignedId::Counter(id));
|
||||
}
|
||||
|
||||
pub fn push_change_id(&mut self, account_id: u32, change_id: u64) {
|
||||
self.ids.push(AssignedId::ChangeId(ChangeId {
|
||||
account_id,
|
||||
change_id,
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn last_change_id(&self, account_id: u32) -> trc::Result<u64> {
|
||||
self.ids
|
||||
.iter()
|
||||
.filter_map(|id| match id {
|
||||
AssignedId::ChangeId(change_id) if change_id.account_id == account_id => {
|
||||
Some(change_id.change_id)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.next_back()
|
||||
.ok_or_else(|| {
|
||||
trc::StoreEvent::UnexpectedError
|
||||
.caused_by(trc::location!())
|
||||
.ctx(trc::Key::Reason, "No change ids were created")
|
||||
})
|
||||
}
|
||||
|
||||
pub fn current_change_id(&self) -> trc::Result<u64> {
|
||||
self.current_change_id.ok_or_else(|| {
|
||||
trc::StoreEvent::UnexpectedError
|
||||
.caused_by(trc::location!())
|
||||
.ctx(trc::Key::Reason, "No current change id is set")
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn set_current_change_id(&mut self, account_id: u32) -> trc::Result<u64> {
|
||||
let change_id = self.last_change_id(account_id)?;
|
||||
self.current_change_id = Some(change_id);
|
||||
Ok(change_id)
|
||||
}
|
||||
|
||||
pub fn last_counter_id(&self) -> trc::Result<i64> {
|
||||
self.ids
|
||||
.iter()
|
||||
.filter_map(|id| match id {
|
||||
AssignedId::Counter(counter_id) => Some(*counter_id),
|
||||
_ => None,
|
||||
})
|
||||
.next_back()
|
||||
.ok_or_else(|| {
|
||||
trc::StoreEvent::UnexpectedError
|
||||
.caused_by(trc::location!())
|
||||
.ctx(trc::Key::Reason, "No counter ids were created")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]>> AsRef<[u8]> for Archive<T> {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.inner.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl ArchiveVersion {
|
||||
pub fn hash(&self) -> Option<u32> {
|
||||
match self {
|
||||
ArchiveVersion::Versioned { hash, .. } => Some(*hash),
|
||||
ArchiveVersion::Hashed { hash } => Some(*hash),
|
||||
ArchiveVersion::Unversioned => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn change_id(&self) -> Option<u64> {
|
||||
match self {
|
||||
ArchiveVersion::Versioned { change_id, .. } => Some(*change_id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LogCollection> for u8 {
|
||||
fn from(value: LogCollection) -> Self {
|
||||
match value {
|
||||
LogCollection::Sync(col) => col as u8,
|
||||
LogCollection::Vanished(col) => col as u8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ContactField> for ValueClass {
|
||||
fn from(value: ContactField) -> Self {
|
||||
ValueClass::Property(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CalendarEventField> for ValueClass {
|
||||
fn from(value: CalendarEventField) -> Self {
|
||||
ValueClass::Property(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CalendarNotificationField> for ValueClass {
|
||||
fn from(value: CalendarNotificationField) -> Self {
|
||||
ValueClass::Property(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EmailField> for ValueClass {
|
||||
fn from(value: EmailField) -> Self {
|
||||
ValueClass::Property(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MailboxField> for ValueClass {
|
||||
fn from(value: MailboxField) -> Self {
|
||||
ValueClass::Property(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PrincipalField> for ValueClass {
|
||||
fn from(value: PrincipalField) -> Self {
|
||||
ValueClass::Property(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SieveField> for ValueClass {
|
||||
fn from(value: SieveField) -> Self {
|
||||
ValueClass::Property(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EmailSubmissionField> for ValueClass {
|
||||
fn from(value: EmailSubmissionField) -> Self {
|
||||
ValueClass::Property(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Field> for ValueClass {
|
||||
fn from(value: Field) -> Self {
|
||||
ValueClass::Property(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for MergeOperation {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.params == other.params
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for MergeOperation {}
|
||||
|
||||
impl PartialEq for SetOperation {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.params == other.params
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for SetOperation {}
|
||||
|
||||
impl Hash for MergeOperation {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.params.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for SetOperation {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.params.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl SetOperation {
|
||||
pub fn params(&self) -> &Params {
|
||||
&self.params
|
||||
}
|
||||
}
|
||||
|
||||
impl MergeOperation {
|
||||
pub fn params(&self) -> &Params {
|
||||
&self.params
|
||||
}
|
||||
}
|
||||
|
||||
impl Params {
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self(Vec::with_capacity(capacity))
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self(Vec::new())
|
||||
}
|
||||
|
||||
pub fn with_i64(mut self, value: i64) -> Self {
|
||||
self.0.push(Param::I64(value));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_u64(mut self, value: u64) -> Self {
|
||||
self.0.push(Param::U64(value));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_string(mut self, value: String) -> Self {
|
||||
self.0.push(Param::String(value));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_str(mut self, value: &str) -> Self {
|
||||
self.0.push(Param::String(value.to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_bytes(mut self, value: Vec<u8>) -> Self {
|
||||
self.0.push(Param::Bytes(value));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_bool(mut self, value: bool) -> Self {
|
||||
self.0.push(Param::Bool(value));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn i64(&self, idx: usize) -> i64 {
|
||||
match &self.0[idx] {
|
||||
Param::I64(v) => *v,
|
||||
_ => panic!("Param at index {} is not an i64", idx),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn u64(&self, idx: usize) -> u64 {
|
||||
match &self.0[idx] {
|
||||
Param::U64(v) => *v,
|
||||
_ => panic!("Param at index {} is not a u64", idx),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn string(&self, idx: usize) -> &str {
|
||||
match &self.0[idx] {
|
||||
Param::String(v) => v.as_str(),
|
||||
_ => panic!("Param at index {} is not a String", idx),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bytes(&self, idx: usize) -> &[u8] {
|
||||
match &self.0[idx] {
|
||||
Param::Bytes(v) => v.as_slice(),
|
||||
_ => panic!("Param at index {} is not Bytes", idx),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bool(&self, idx: usize) -> bool {
|
||||
match &self.0[idx] {
|
||||
Param::Bool(v) => *v,
|
||||
_ => panic!("Param at index {} is not a bool", idx),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn as_slice(&self) -> &[Param] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Params {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[Param]> for Params {
|
||||
fn as_ref(&self) -> &[Param] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{ARCHIVE_ALIGNMENT, AlignedBytes, Archive, ArchiveVersion, Archiver};
|
||||
use crate::{Deserialize, Serialize, SerializeInfallible, U32_LEN, U64_LEN, Value};
|
||||
use compact_str::format_compact;
|
||||
use rkyv::util::AlignedVec;
|
||||
use roaring::{RoaringBitmap, RoaringTreemap};
|
||||
|
||||
const MAGIC_MARKER: u8 = 1 << 7;
|
||||
const VERSIONED: u8 = 1 << 6;
|
||||
const HASHED: u8 = 1 << 5;
|
||||
const LZ4_COMPRESSED: u8 = 1 << 4;
|
||||
|
||||
const COMPRESS_WATERMARK: usize = 8192;
|
||||
|
||||
fn validate_marker_and_contents(bytes: &[u8]) -> Option<(bool, &[u8], ArchiveVersion)> {
|
||||
let (marker, contents) = bytes
|
||||
.split_last()
|
||||
.filter(|(marker, _)| (**marker & MAGIC_MARKER) != 0)?;
|
||||
let is_uncompressed = (marker & LZ4_COMPRESSED) == 0;
|
||||
if marker & VERSIONED != 0 {
|
||||
let (contents, change_id) = contents
|
||||
.split_at_checked(contents.len() - U64_LEN)
|
||||
.and_then(|(contents, change_id)| {
|
||||
change_id
|
||||
.try_into()
|
||||
.ok()
|
||||
.map(|change_id| (contents, u64::from_be_bytes(change_id)))
|
||||
})?;
|
||||
contents
|
||||
.split_at_checked(contents.len() - U32_LEN)
|
||||
.and_then(|(contents, archive_hash)| {
|
||||
let hash = xxhash_rust::xxh3::xxh3_64(contents) as u32;
|
||||
if hash.to_be_bytes().as_slice() == archive_hash {
|
||||
Some((
|
||||
is_uncompressed,
|
||||
contents,
|
||||
ArchiveVersion::Versioned { change_id, hash },
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
} else if marker & HASHED != 0 {
|
||||
contents
|
||||
.split_at_checked(contents.len() - U32_LEN)
|
||||
.and_then(|(contents, archive_hash)| {
|
||||
let hash = xxhash_rust::xxh3::xxh3_64(contents) as u32;
|
||||
if hash.to_be_bytes().as_slice() == archive_hash {
|
||||
Some((is_uncompressed, contents, ArchiveVersion::Hashed { hash }))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Some((is_uncompressed, contents, ArchiveVersion::Unversioned))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for Archive<AlignedBytes> {
|
||||
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
|
||||
let (is_uncompressed, contents, version) =
|
||||
validate_marker_and_contents(bytes).ok_or_else(|| {
|
||||
trc::StoreEvent::DataCorruption
|
||||
.into_err()
|
||||
.details("Archive integrity compromised")
|
||||
.ctx(trc::Key::Value, bytes)
|
||||
.caused_by(trc::location!())
|
||||
})?;
|
||||
|
||||
if is_uncompressed {
|
||||
let mut bytes = AlignedVec::with_capacity(contents.len());
|
||||
bytes.extend_from_slice(contents);
|
||||
Ok(Archive {
|
||||
version,
|
||||
inner: AlignedBytes::Aligned(bytes),
|
||||
})
|
||||
} else {
|
||||
aligned_lz4_deflate(contents).map(|inner| Archive { version, inner })
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_owned(mut bytes: Vec<u8>) -> trc::Result<Self> {
|
||||
let (is_uncompressed, contents, version) = validate_marker_and_contents(&bytes)
|
||||
.ok_or_else(|| {
|
||||
trc::StoreEvent::DataCorruption
|
||||
.into_err()
|
||||
.details("Archive integrity compromised")
|
||||
.ctx(trc::Key::Value, bytes.as_slice())
|
||||
.caused_by(trc::location!())
|
||||
})?;
|
||||
|
||||
if is_uncompressed {
|
||||
bytes.truncate(contents.len());
|
||||
if bytes.as_ptr().addr() & (ARCHIVE_ALIGNMENT - 1) == 0 {
|
||||
Ok(Archive {
|
||||
version,
|
||||
inner: AlignedBytes::Vec(bytes),
|
||||
})
|
||||
} else {
|
||||
let mut aligned = AlignedVec::with_capacity(bytes.len());
|
||||
aligned.extend_from_slice(&bytes);
|
||||
Ok(Archive {
|
||||
version,
|
||||
inner: AlignedBytes::Aligned(aligned),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
aligned_lz4_deflate(contents).map(|inner| Archive { version, inner })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn aligned_lz4_deflate(archive: &[u8]) -> trc::Result<AlignedBytes> {
|
||||
lz4_flex::block::uncompressed_size(archive)
|
||||
.and_then(|(uncompressed_size, archive)| {
|
||||
let mut bytes = AlignedVec::with_capacity(uncompressed_size);
|
||||
unsafe {
|
||||
// SAFETY: `new_len` is equal to `capacity` and vector is initialized by lz4_flex.
|
||||
bytes.set_len(uncompressed_size);
|
||||
}
|
||||
lz4_flex::decompress_into(archive, &mut bytes)?;
|
||||
Ok(AlignedBytes::Aligned(bytes))
|
||||
})
|
||||
.map_err(|err| {
|
||||
trc::StoreEvent::DecompressError
|
||||
.ctx(trc::Key::Value, archive)
|
||||
.caused_by(trc::location!())
|
||||
.reason(err)
|
||||
})
|
||||
}
|
||||
|
||||
impl<T> Serialize for Archiver<T>
|
||||
where
|
||||
T: rkyv::Archive
|
||||
+ for<'a> rkyv::Serialize<
|
||||
rkyv::api::high::HighSerializer<
|
||||
rkyv::util::AlignedVec,
|
||||
rkyv::ser::allocator::ArenaHandle<'a>,
|
||||
rkyv::rancor::Error,
|
||||
>,
|
||||
>,
|
||||
{
|
||||
fn serialize(&self) -> trc::Result<Vec<u8>> {
|
||||
rkyv::to_bytes::<rkyv::rancor::Error>(&self.inner)
|
||||
.map_err(|err| {
|
||||
trc::StoreEvent::DeserializeError
|
||||
.caused_by(trc::location!())
|
||||
.reason(err)
|
||||
})
|
||||
.map(|input| {
|
||||
let input = input.as_ref();
|
||||
let input_len = input.len();
|
||||
let version_offset = ((self.flags & VERSIONED != 0) as usize) * U64_LEN;
|
||||
let mut bytes = if input_len > COMPRESS_WATERMARK {
|
||||
let mut bytes = vec![
|
||||
self.flags | LZ4_COMPRESSED;
|
||||
lz4_flex::block::get_maximum_output_size(input_len)
|
||||
+ (U32_LEN * 2)
|
||||
+ version_offset
|
||||
+ 1
|
||||
];
|
||||
|
||||
// Compress the data
|
||||
let compressed_len =
|
||||
lz4_flex::compress_into(input, &mut bytes[U32_LEN..]).unwrap();
|
||||
|
||||
if compressed_len < input_len {
|
||||
// Prepend the length of the uncompressed data
|
||||
bytes[..U32_LEN].copy_from_slice(&(input_len as u32).to_le_bytes());
|
||||
|
||||
if self.flags & HASHED != 0 {
|
||||
// Hash the compressed data including the length
|
||||
let hash =
|
||||
xxhash_rust::xxh3::xxh3_64(&bytes[..compressed_len + U32_LEN])
|
||||
as u32;
|
||||
|
||||
// Add the hash
|
||||
bytes[compressed_len + U32_LEN..compressed_len + (U32_LEN * 2)]
|
||||
.copy_from_slice(&hash.to_be_bytes());
|
||||
|
||||
// Truncate to the actual size
|
||||
bytes.truncate(compressed_len + (U32_LEN * 2) + version_offset + 1);
|
||||
} else {
|
||||
// Truncate to the actual size
|
||||
bytes.truncate(compressed_len + U32_LEN + 1);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
bytes.clear();
|
||||
bytes
|
||||
} else {
|
||||
Vec::with_capacity(input_len + U32_LEN + version_offset + 1)
|
||||
};
|
||||
|
||||
bytes.extend_from_slice(input);
|
||||
if self.flags & HASHED != 0 {
|
||||
bytes.extend_from_slice(
|
||||
&(xxhash_rust::xxh3::xxh3_64(input) as u32).to_be_bytes(),
|
||||
);
|
||||
}
|
||||
if version_offset != 0 {
|
||||
bytes.extend_from_slice(0u64.to_be_bytes().as_slice());
|
||||
}
|
||||
bytes.push(self.flags);
|
||||
bytes
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Archive<AlignedBytes> {
|
||||
#[inline]
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
match &self.inner {
|
||||
AlignedBytes::Vec(bytes) => bytes.as_slice(),
|
||||
AlignedBytes::Aligned(bytes) => bytes.as_slice(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unarchive<T>(&self) -> trc::Result<&<T as rkyv::Archive>::Archived>
|
||||
where
|
||||
T: rkyv::Archive,
|
||||
T::Archived: for<'a> rkyv::bytecheck::CheckBytes<
|
||||
rkyv::api::high::HighValidator<'a, rkyv::rancor::Error>,
|
||||
> + rkyv::Deserialize<T, rkyv::api::high::HighDeserializer<rkyv::rancor::Error>>,
|
||||
{
|
||||
let bytes = self.as_bytes();
|
||||
if self.version != ArchiveVersion::Unversioned {
|
||||
if bytes.len() >= std::mem::size_of::<T::Archived>() {
|
||||
// SAFETY: Trusted input with integrity hash
|
||||
Ok(unsafe { rkyv::access_unchecked::<T::Archived>(bytes) })
|
||||
} else {
|
||||
Err(trc::StoreEvent::DataCorruption
|
||||
.into_err()
|
||||
.details(format_compact!(
|
||||
"Archive size mismatch, expected {} bytes but got {} bytes.",
|
||||
std::mem::size_of::<T::Archived>(),
|
||||
bytes.len()
|
||||
))
|
||||
.ctx(trc::Key::Value, bytes)
|
||||
.caused_by(trc::location!()))
|
||||
}
|
||||
} else {
|
||||
rkyv::access::<T::Archived, rkyv::rancor::Error>(bytes).map_err(|err| {
|
||||
trc::StoreEvent::DeserializeError
|
||||
.ctx(trc::Key::Value, self.as_bytes())
|
||||
.details("Archive access failed")
|
||||
.caused_by(trc::location!())
|
||||
.reason(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unarchive_untrusted<T>(&self) -> trc::Result<&<T as rkyv::Archive>::Archived>
|
||||
where
|
||||
T: rkyv::Archive,
|
||||
T::Archived: for<'a> rkyv::bytecheck::CheckBytes<
|
||||
rkyv::api::high::HighValidator<'a, rkyv::rancor::Error>,
|
||||
> + rkyv::Deserialize<T, rkyv::api::high::HighDeserializer<rkyv::rancor::Error>>,
|
||||
{
|
||||
let bytes = self.as_bytes();
|
||||
if bytes.len() >= std::mem::size_of::<T::Archived>() {
|
||||
rkyv::access::<T::Archived, rkyv::rancor::Error>(bytes).map_err(|err| {
|
||||
trc::StoreEvent::DeserializeError
|
||||
.ctx(trc::Key::Value, self.as_bytes())
|
||||
.details("Archive access failed")
|
||||
.caused_by(trc::location!())
|
||||
.reason(err)
|
||||
})
|
||||
} else {
|
||||
Err(trc::StoreEvent::DataCorruption
|
||||
.into_err()
|
||||
.details(format_compact!(
|
||||
"Archive size mismatch, expected {} bytes but got {} bytes.",
|
||||
std::mem::size_of::<T::Archived>(),
|
||||
bytes.len()
|
||||
))
|
||||
.ctx(trc::Key::Value, bytes)
|
||||
.caused_by(trc::location!()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize<T>(&self) -> trc::Result<T>
|
||||
where
|
||||
T: rkyv::Archive,
|
||||
T::Archived: for<'a> rkyv::bytecheck::CheckBytes<
|
||||
rkyv::api::high::HighValidator<'a, rkyv::rancor::Error>,
|
||||
> + rkyv::Deserialize<T, rkyv::api::high::HighDeserializer<rkyv::rancor::Error>>,
|
||||
{
|
||||
self.unarchive::<T>().and_then(|input| {
|
||||
rkyv::deserialize(input).map_err(|err| {
|
||||
trc::StoreEvent::DeserializeError
|
||||
.ctx(trc::Key::Value, self.as_bytes())
|
||||
.caused_by(trc::location!())
|
||||
.reason(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn deserialize_untrusted<T>(&self) -> trc::Result<T>
|
||||
where
|
||||
T: rkyv::Archive,
|
||||
T::Archived: for<'a> rkyv::bytecheck::CheckBytes<
|
||||
rkyv::api::high::HighValidator<'a, rkyv::rancor::Error>,
|
||||
> + rkyv::Deserialize<T, rkyv::api::high::HighDeserializer<rkyv::rancor::Error>>,
|
||||
{
|
||||
self.unarchive_untrusted::<T>().and_then(|input| {
|
||||
rkyv::deserialize(input).map_err(|err| {
|
||||
trc::StoreEvent::DeserializeError
|
||||
.ctx(trc::Key::Value, self.as_bytes())
|
||||
.caused_by(trc::location!())
|
||||
.reason(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_unarchived<T>(&self) -> trc::Result<Archive<&<T as rkyv::Archive>::Archived>>
|
||||
where
|
||||
T: rkyv::Archive,
|
||||
T::Archived: for<'a> rkyv::bytecheck::CheckBytes<
|
||||
rkyv::api::high::HighValidator<'a, rkyv::rancor::Error>,
|
||||
> + rkyv::Deserialize<T, rkyv::api::high::HighDeserializer<rkyv::rancor::Error>>,
|
||||
{
|
||||
self.unarchive::<T>().map(|inner| Archive {
|
||||
version: self.version,
|
||||
inner,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn into_deserialized<T>(&self) -> trc::Result<Archive<T>>
|
||||
where
|
||||
T: rkyv::Archive,
|
||||
T::Archived: for<'a> rkyv::bytecheck::CheckBytes<
|
||||
rkyv::api::high::HighValidator<'a, rkyv::rancor::Error>,
|
||||
> + rkyv::Deserialize<T, rkyv::api::high::HighDeserializer<rkyv::rancor::Error>>,
|
||||
{
|
||||
self.deserialize::<T>().map(|inner| Archive {
|
||||
version: self.version,
|
||||
inner,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> Vec<u8> {
|
||||
let mut bytes = match self.inner {
|
||||
AlignedBytes::Vec(bytes) => bytes,
|
||||
AlignedBytes::Aligned(bytes) => bytes.to_vec(),
|
||||
};
|
||||
match self.version {
|
||||
ArchiveVersion::Versioned { change_id, hash } => {
|
||||
bytes.extend_from_slice(&hash.to_be_bytes());
|
||||
bytes.extend_from_slice(&change_id.to_be_bytes());
|
||||
bytes.push(MAGIC_MARKER | VERSIONED | HASHED);
|
||||
}
|
||||
ArchiveVersion::Hashed { hash } => {
|
||||
bytes.extend_from_slice(&hash.to_be_bytes());
|
||||
bytes.push(MAGIC_MARKER | HASHED);
|
||||
}
|
||||
ArchiveVersion::Unversioned => {
|
||||
bytes.push(MAGIC_MARKER);
|
||||
}
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn extract_hash(bytes: &[u8]) -> Option<u32> {
|
||||
let marker = *bytes.last()?;
|
||||
if marker & VERSIONED != 0 {
|
||||
bytes
|
||||
.get(bytes.len() - U32_LEN - U64_LEN - 1..bytes.len() - U64_LEN - 1)
|
||||
.and_then(|slice| slice.try_into().ok().map(u32::from_be_bytes))
|
||||
} else if marker & HASHED != 0 {
|
||||
bytes
|
||||
.get(bytes.len() - U32_LEN - 1..bytes.len() - 1)
|
||||
.and_then(|slice| slice.try_into().ok().map(u32::from_be_bytes))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Archiver<T>
|
||||
where
|
||||
T: rkyv::Archive
|
||||
+ for<'a> rkyv::Serialize<
|
||||
rkyv::api::high::HighSerializer<
|
||||
rkyv::util::AlignedVec,
|
||||
rkyv::ser::allocator::ArenaHandle<'a>,
|
||||
rkyv::rancor::Error,
|
||||
>,
|
||||
>,
|
||||
{
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
flags: MAGIC_MARKER | HASHED,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> T {
|
||||
self.inner
|
||||
}
|
||||
|
||||
pub fn with_version(self) -> Self {
|
||||
Self {
|
||||
inner: self.inner,
|
||||
flags: self.flags | VERSIONED,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn untrusted(self) -> Self {
|
||||
Self {
|
||||
inner: self.inner,
|
||||
flags: MAGIC_MARKER,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_versioned(self) -> trc::Result<(u64, Vec<u8>)> {
|
||||
self.with_version()
|
||||
.serialize()
|
||||
.map(|bytes| ((bytes.len() - U64_LEN - 1) as u64, bytes))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Archive<&T>
|
||||
where
|
||||
T: rkyv::Portable
|
||||
+ for<'a> rkyv::bytecheck::CheckBytes<rkyv::api::high::HighValidator<'a, rkyv::rancor::Error>>
|
||||
+ Sync
|
||||
+ Send,
|
||||
{
|
||||
pub fn to_deserialized<V>(&self) -> trc::Result<Archive<V>>
|
||||
where
|
||||
T: rkyv::Deserialize<V, rkyv::api::high::HighDeserializer<rkyv::rancor::Error>>,
|
||||
{
|
||||
rkyv::deserialize::<V, rkyv::rancor::Error>(self.inner)
|
||||
.map_err(|err| {
|
||||
trc::StoreEvent::DeserializeError
|
||||
.caused_by(trc::location!())
|
||||
.reason(err)
|
||||
})
|
||||
.map(|inner| Archive {
|
||||
version: self.version,
|
||||
inner,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn deserialize<V>(&self) -> trc::Result<V>
|
||||
where
|
||||
T: rkyv::Deserialize<V, rkyv::api::high::HighDeserializer<rkyv::rancor::Error>>,
|
||||
{
|
||||
rkyv::deserialize::<V, rkyv::rancor::Error>(self.inner).map_err(|err| {
|
||||
trc::StoreEvent::DeserializeError
|
||||
.caused_by(trc::location!())
|
||||
.reason(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn rkyv_deserialize<T, V>(input: &T) -> trc::Result<V>
|
||||
where
|
||||
T: rkyv::Portable
|
||||
+ for<'a> rkyv::bytecheck::CheckBytes<rkyv::api::high::HighValidator<'a, rkyv::rancor::Error>>
|
||||
+ Sync
|
||||
+ Send
|
||||
+ rkyv::Deserialize<V, rkyv::api::high::HighDeserializer<rkyv::rancor::Error>>,
|
||||
{
|
||||
rkyv::deserialize::<V, rkyv::rancor::Error>(input).map_err(|err| {
|
||||
trc::StoreEvent::DeserializeError
|
||||
.caused_by(trc::location!())
|
||||
.reason(err)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn rkyv_unarchive<T>(input: &[u8]) -> trc::Result<&<T as rkyv::Archive>::Archived>
|
||||
where
|
||||
T: rkyv::Archive,
|
||||
T::Archived: for<'a> rkyv::bytecheck::CheckBytes<rkyv::api::high::HighValidator<'a, rkyv::rancor::Error>>
|
||||
+ rkyv::Deserialize<T, rkyv::api::high::HighDeserializer<rkyv::rancor::Error>>,
|
||||
{
|
||||
rkyv::access::<T::Archived, rkyv::rancor::Error>(input).map_err(|err| {
|
||||
trc::StoreEvent::DataCorruption
|
||||
.caused_by(trc::location!())
|
||||
.ctx(trc::Key::Value, input)
|
||||
.reason(err)
|
||||
})
|
||||
}
|
||||
|
||||
impl SerializeInfallible for u32 {
|
||||
fn serialize(&self) -> Vec<u8> {
|
||||
self.to_be_bytes().to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializeInfallible for u64 {
|
||||
fn serialize(&self) -> Vec<u8> {
|
||||
self.to_be_bytes().to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializeInfallible for i64 {
|
||||
fn serialize(&self) -> Vec<u8> {
|
||||
self.to_be_bytes().to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializeInfallible for u16 {
|
||||
fn serialize(&self) -> Vec<u8> {
|
||||
self.to_be_bytes().to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializeInfallible for f64 {
|
||||
fn serialize(&self) -> Vec<u8> {
|
||||
self.to_be_bytes().to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializeInfallible for &str {
|
||||
fn serialize(&self) -> Vec<u8> {
|
||||
self.as_bytes().to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for String {
|
||||
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
|
||||
Ok(String::from_utf8_lossy(bytes).into_owned())
|
||||
}
|
||||
|
||||
fn deserialize_owned(bytes: Vec<u8>) -> trc::Result<Self> {
|
||||
Ok(String::from_utf8(bytes)
|
||||
.unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for u64 {
|
||||
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
|
||||
Ok(u64::from_be_bytes(bytes.try_into().map_err(|_| {
|
||||
trc::StoreEvent::DataCorruption.caused_by(trc::location!())
|
||||
})?))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for i64 {
|
||||
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
|
||||
Ok(i64::from_be_bytes(bytes.try_into().map_err(|_| {
|
||||
trc::StoreEvent::DataCorruption.caused_by(trc::location!())
|
||||
})?))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for u32 {
|
||||
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
|
||||
Ok(u32::from_be_bytes(bytes.try_into().map_err(|_| {
|
||||
trc::StoreEvent::DataCorruption.caused_by(trc::location!())
|
||||
})?))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Value<'static>> for Archive<T> {
|
||||
fn from(_: Value<'static>) -> Self {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Archive<AlignedBytes> {
|
||||
fn default() -> Self {
|
||||
Archive {
|
||||
version: ArchiveVersion::Unversioned,
|
||||
inner: AlignedBytes::Aligned(AlignedVec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for RoaringBitmap {
|
||||
fn serialize(&self) -> trc::Result<Vec<u8>> {
|
||||
let mut bytes = Vec::with_capacity(self.serialized_size());
|
||||
self.serialize_into(&mut bytes)
|
||||
.map_err(|err| {
|
||||
trc::StoreEvent::UnexpectedError
|
||||
.caused_by(trc::location!())
|
||||
.reason(err)
|
||||
})
|
||||
.map(|_| bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for RoaringBitmap {
|
||||
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
|
||||
RoaringBitmap::deserialize_from(bytes).map_err(|err| {
|
||||
trc::StoreEvent::DeserializeError
|
||||
.caused_by(trc::location!())
|
||||
.reason(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for RoaringTreemap {
|
||||
fn serialize(&self) -> trc::Result<Vec<u8>> {
|
||||
let mut bytes = Vec::with_capacity(self.serialized_size());
|
||||
self.serialize_into(&mut bytes)
|
||||
.map_err(|err| {
|
||||
trc::StoreEvent::UnexpectedError
|
||||
.caused_by(trc::location!())
|
||||
.reason(err)
|
||||
})
|
||||
.map(|_| bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for RoaringTreemap {
|
||||
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
|
||||
RoaringTreemap::deserialize_from(bytes).map_err(|err| {
|
||||
trc::StoreEvent::DeserializeError
|
||||
.caused_by(trc::location!())
|
||||
.reason(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user