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:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+78
View File
@@ -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
+169
View File
@@ -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)
}
+154
View File
@@ -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)),
}
}
+142
View File
@@ -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)
}
+377
View File
@@ -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(),
}),
}))
}
}
+22
View File
@@ -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),
}
}
}
+200
View File
@@ -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
}
+111
View File
@@ -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)
}
+72
View File
@@ -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
),
);
}
}
}
}
}
+232
View File
@@ -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)
}
}
+52
View File
@@ -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,
}
+332
View File
@@ -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)),
}
}
+69
View File
@@ -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,
}
+561
View File
@@ -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!(),
}
}
}
+65
View File
@@ -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()),
);
}
}
}
+39
View File
@@ -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!())
})?))
}
+64
View File
@@ -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)
}
}
+136
View File
@@ -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!()
}
}
+212
View File
@@ -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(())
}
+208
View File
@@ -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(),
}
}
}
+169
View File
@@ -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)),
}
}
}
+353
View File
@@ -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),
}
}
+529
View File
@@ -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)
}
}
+69
View File
@@ -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)
}
}
+201
View File
@@ -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
}
}
+265
View File
@@ -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()
}
+311
View File
@@ -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,
}
}
}
+168
View File
@@ -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)),
}
}
}
+571
View File
@@ -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,
}
}
+198
View File
@@ -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)
}
}
+543
View File
@@ -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)
}
}
+301
View File
@@ -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(());
}
}
}
}
+215
View File
@@ -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(),
}
}
}
+87
View File
@@ -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)))
}
}
+55
View File
@@ -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
}
}
+230
View File
@@ -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
}
+43
View File
@@ -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)
}
+132
View File
@@ -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
}
}
+306
View File
@@ -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)
}
}
+259
View File
@@ -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)
}
+70
View File
@@ -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
}
}
+145
View File
@@ -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!()
}
}
+146
View File
@@ -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)),
}
}
}
+26
View File
@@ -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)
}
+118
View File
@@ -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
}
}
+152
View File
@@ -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
}
}
+319
View File
@@ -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
}
}
+54
View File
@@ -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
}
}
}
}
+316
View File
@@ -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,
}
}
}
+95
View File
@@ -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
}
}
}
}
+78
View File
@@ -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);
}
}
}
}
}
+41
View File
@@ -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
}
}
}
}
+13
View File
@@ -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;
+165
View File
@@ -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()
}
}
+56
View File
@@ -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
}
}
}
}
+195
View File
@@ -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
}
}
+625
View File
@@ -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(),
}
}
}
+107
View File
@@ -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()
}
}
+337
View File
@@ -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 { .. })
}
}
+373
View File
@@ -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, &params).await,
#[cfg(feature = "postgres")]
Self::PostgreSQL(store) => store.sql_query(query, &params).await,
#[cfg(feature = "mysql")]
Self::MySQL(store) => store.sql_query(query, &params).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();
}
}
}
+737
View File
@@ -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!()
}
}
+165
View File
@@ -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
}
}
+487
View File
@@ -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()
}
}
+52
View File
@@ -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
}
}
+206
View File
@@ -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();
}
}
}
+105
View File
@@ -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)
}
}
+108
View File
@@ -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)
})
}
}
+238
View File
@@ -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)
})
}
}
+943
View File
@@ -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)
}
}
+632
View File
@@ -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"),
}
}
}
+278
View File
@@ -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)
}
+205
View File
@@ -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)
}
}
+315
View File
@@ -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
}
+249
View File
@@ -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!(),
}
}
}
+336
View File
@@ -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(())
}
}
+228
View File
@@ -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
}
}
+340
View File
@@ -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",
},
}
}
}
+423
View File
@@ -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<_>>())
}
}
}
+708
View File
@@ -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"),
}
}
}
+451
View File
@@ -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,
},
}
}
}
+88
View File
@@ -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)
}
}
+556
View File
@@ -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()
}
}
+382
View File
@@ -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");
}
}
}
+296
View File
@@ -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!(),
)),
}
}
}
+685
View File
@@ -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,
}
}
}
+234
View File
@@ -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()
}
}
+700
View File
@@ -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
}
}
+625
View File
@@ -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)
})
}
}