Groupware caching improvements

This commit is contained in:
mdecimus
2025-05-08 17:30:57 +02:00
parent e69b5ec2b7
commit fe7d646966
54 changed files with 2138 additions and 1963 deletions

View File

@@ -116,9 +116,23 @@ impl Caches {
+ (1024 * std::mem::size_of::<MessageUidCache>())
+ (15 * (std::mem::size_of::<MailboxCache>() + 60))) as u64,
),
dav: Cache::from_config(
files: Cache::from_config(
config,
"dav",
"files",
MB_10,
(std::mem::size_of::<DavResources>() + (500 * std::mem::size_of::<DavResource>()))
as u64,
),
events: Cache::from_config(
config,
"events",
MB_10,
(std::mem::size_of::<DavResources>() + (500 * std::mem::size_of::<DavResource>()))
as u64,
),
contacts: Cache::from_config(
config,
"contacts",
MB_10,
(std::mem::size_of::<DavResources>() + (500 * std::mem::size_of::<DavResource>()))
as u64,

View File

@@ -33,7 +33,7 @@ use nlp::bayes::{TokenHash, Weights};
use parking_lot::{Mutex, RwLock};
use rustls::sign::CertifiedKey;
use std::{
hash::{BuildHasher, Hasher},
hash::{BuildHasher, Hash, Hasher},
net::{IpAddr, Ipv4Addr, Ipv6Addr},
sync::{
Arc,
@@ -46,7 +46,6 @@ use tinyvec::TinyVec;
use tokio::sync::{Notify, Semaphore, mpsc};
use tokio_rustls::TlsConnector;
use utils::{
bimap::{IdBimap, IdBimapItem},
cache::{Cache, CacheItemWeight, CacheWithTtl},
snowflake::SnowflakeIdGenerator,
};
@@ -149,7 +148,9 @@ pub struct Caches {
pub permissions: Cache<u32, Arc<RolePermissions>>,
pub messages: Cache<u32, CacheSwap<MessageStoreCache>>,
pub dav: Cache<DavResourceId, Arc<DavResources>>,
pub files: Cache<u32, CacheSwap<DavResources>>,
pub contacts: Cache<u32, CacheSwap<DavResources>>,
pub events: Cache<u32, CacheSwap<DavResources>>,
pub bayes: CacheWithTtl<TokenHash, Weights>,
@@ -242,44 +243,72 @@ pub struct TlsConnectors {
pub struct NameWrapper(pub String);
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub struct DavResourceId {
pub account_id: u32,
pub collection: u8,
}
#[derive(Debug, Default)]
#[derive(Debug, Clone)]
pub struct DavResources {
pub base_path: String,
pub paths: IdBimap<DavResource>,
pub paths: AHashSet<DavPath>,
pub resources: Vec<DavResource>,
pub item_change_id: u64,
pub container_change_id: u64,
pub highest_change_id: u64,
pub size: u64,
pub modseq: Option<u64>,
pub update_lock: Arc<Semaphore>,
}
#[derive(Debug, Default, Clone)]
#[derive(Debug, Clone)]
pub struct DavPath {
pub path: String,
pub parent_id: Option<u32>,
pub hierarchy_seq: u32,
pub resource_idx: usize,
}
#[derive(Debug, Clone)]
pub struct DavResource {
pub document_id: u32,
pub parent_id: Option<u32>,
pub name: String,
pub data: DavResourceMetadata,
}
#[derive(Debug, Default, Clone)]
#[derive(Debug, Clone, Copy)]
pub struct DavResourcePath<'x> {
pub path: &'x DavPath,
pub resource: &'x DavResource,
}
#[derive(Debug, Clone)]
pub enum DavResourceMetadata {
File {
size: u32,
hierarchy_sequence: u32,
is_container: bool,
name: String,
size: Option<u32>,
parent_id: Option<u32>,
acls: TinyVec<[AclGrant; 2]>,
},
Calendar {
name: String,
acls: TinyVec<[AclGrant; 2]>,
tz: Tz,
},
CalendarEvent {
names: TinyVec<[DavName; 2]>,
start: i64,
duration: u32,
},
#[default]
None,
AddressBook {
name: String,
acls: TinyVec<[AclGrant; 2]>,
},
ContactCard {
names: TinyVec<[DavName; 2]>,
},
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
#[rkyv(derive(Debug))]
pub struct DavName {
pub name: String,
pub parent_id: u32,
}
#[derive(Clone, Default)]
@@ -299,12 +328,6 @@ pub struct Core {
pub enterprise: Option<enterprise::Enterprise>,
}
impl CacheItemWeight for DavResourceId {
fn weight(&self) -> u64 {
std::mem::size_of::<DavResourceId>() as u64
}
}
impl<T: CacheItemWeight> CacheItemWeight for CacheSwap<T> {
fn weight(&self) -> u64 {
std::mem::size_of::<CacheSwap<T>>() as u64 + self.0.load().weight()
@@ -428,7 +451,9 @@ impl Default for Caches {
http_auth: Cache::new(1024, 10 * 1024 * 1024),
permissions: Cache::new(1024, 10 * 1024 * 1024),
messages: Cache::new(1024, 25 * 1024 * 1024),
dav: Cache::new(1024, 10 * 1024 * 1024),
files: Cache::new(1024, 10 * 1024 * 1024),
contacts: Cache::new(1024, 10 * 1024 * 1024),
events: Cache::new(1024, 10 * 1024 * 1024),
bayes: CacheWithTtl::new(1024, 10 * 1024 * 1024),
dns_rbl: CacheWithTtl::new(1024, 10 * 1024 * 1024),
dns_txt: CacheWithTtl::new(1024, 10 * 1024 * 1024),
@@ -480,45 +505,117 @@ pub fn ip_to_bytes_prefix(prefix: u8, ip: &IpAddr) -> Vec<u8> {
}
}
impl DavResourcePath<'_> {
#[inline(always)]
pub fn document_id(&self) -> u32 {
self.resource.document_id
}
#[inline(always)]
pub fn parent_id(&self) -> Option<u32> {
self.path.parent_id
}
#[inline(always)]
pub fn path(&self) -> &str {
self.path.path.as_str()
}
#[inline(always)]
pub fn is_container(&self) -> bool {
self.resource.is_container()
}
#[inline(always)]
pub fn hierarchy_seq(&self) -> u32 {
self.path.hierarchy_seq
}
#[inline(always)]
pub fn size(&self) -> u32 {
self.resource.size()
}
}
impl DavResources {
pub fn subtree(&self, search_path: &str) -> impl Iterator<Item = &DavResource> {
let prefix = format!("{search_path}/");
self.paths
pub fn by_path(&self, name: &str) -> Option<DavResourcePath<'_>> {
self.paths.get(name).map(|path| DavResourcePath {
path,
resource: &self.resources[path.resource_idx],
})
}
pub fn container_resource_by_id(&self, id: u32) -> Option<&DavResource> {
self.resources
.iter()
.filter(move |item| item.name.starts_with(&prefix) || item.name == search_path)
.find(|res| res.document_id == id && res.is_container())
}
pub fn subtree(&self, search_path: &str) -> impl Iterator<Item = DavResourcePath<'_>> {
let prefix = format!("{search_path}/");
self.paths.iter().filter_map(move |path| {
if path.path.starts_with(&prefix) || path.path == search_path {
Some(DavResourcePath {
path,
resource: &self.resources[path.resource_idx],
})
} else {
None
}
})
}
pub fn subtree_with_depth(
&self,
search_path: &str,
depth: usize,
) -> impl Iterator<Item = &DavResource> {
) -> impl Iterator<Item = DavResourcePath<'_>> {
let prefix = format!("{search_path}/");
self.paths.iter().filter(move |item| {
item.name
self.paths.iter().filter_map(move |path| {
if path
.path
.strip_prefix(&prefix)
.is_some_and(|name| name.as_bytes().iter().filter(|&&c| c == b'/').count() < depth)
|| item.name == search_path
|| path.path.as_str() == search_path
{
Some(DavResourcePath {
path,
resource: &self.resources[path.resource_idx],
})
} else {
None
}
})
}
pub fn tree_with_depth(&self, depth: usize) -> impl Iterator<Item = &DavResource> {
self.paths.iter().filter(move |item| {
item.name.as_bytes().iter().filter(|&&c| c == b'/').count() <= depth
pub fn tree_with_depth(&self, depth: usize) -> impl Iterator<Item = DavResourcePath<'_>> {
self.paths.iter().filter_map(move |path| {
if path.path.as_bytes().iter().filter(|&&c| c == b'/').count() <= depth {
Some(DavResourcePath {
path,
resource: &self.resources[path.resource_idx],
})
} else {
None
}
})
}
pub fn children(&self, parent_id: u32) -> impl Iterator<Item = &DavResource> {
pub fn children(&self, parent_id: u32) -> impl Iterator<Item = DavResourcePath<'_>> {
self.paths
.iter()
.filter(move |item| item.parent_id.is_some_and(|id| id == parent_id))
.map(|path| DavResourcePath {
path,
resource: &self.resources[path.resource_idx],
})
}
pub fn format_resource(&self, resource: &DavResource) -> String {
if resource.is_container() {
format!("{}{}/", self.base_path, resource.name)
pub fn format_resource(&self, resource: DavResourcePath<'_>) -> String {
if resource.resource.is_container() {
format!("{}{}/", self.base_path, resource.path.path)
} else {
format!("{}{}", self.base_path, resource.name)
format!("{}{}", self.base_path, resource.path.path)
}
}
@@ -532,59 +629,128 @@ impl DavResources {
}
impl DavResource {
pub fn is_child_of(&self, parent_id: u32) -> bool {
match &self.data {
DavResourceMetadata::File { parent_id: id, .. } => id.is_some_and(|id| id == parent_id),
DavResourceMetadata::CalendarEvent { names, .. } => {
names.iter().any(|name| name.parent_id == parent_id)
}
DavResourceMetadata::ContactCard { names } => {
names.iter().any(|name| name.parent_id == parent_id)
}
_ => false,
}
}
pub fn child_names(&self) -> Option<&[DavName]> {
match &self.data {
DavResourceMetadata::CalendarEvent { names, .. } => Some(names.as_slice()),
DavResourceMetadata::ContactCard { names } => Some(names.as_slice()),
_ => None,
}
}
pub fn container_name(&self) -> Option<&str> {
match &self.data {
DavResourceMetadata::File { name, .. } => Some(name.as_str()),
DavResourceMetadata::Calendar { name, .. } => Some(name.as_str()),
DavResourceMetadata::AddressBook { name, .. } => Some(name.as_str()),
_ => None,
}
}
pub fn has_hierarchy_changes(&self, other: &DavResource) -> bool {
match (&self.data, &other.data) {
(
DavResourceMetadata::File {
name: a,
parent_id: c,
..
},
DavResourceMetadata::File {
name: b,
parent_id: d,
..
},
) => a != b || c != d,
(
DavResourceMetadata::Calendar { name: a, .. },
DavResourceMetadata::Calendar { name: b, .. },
) => a != b,
(
DavResourceMetadata::AddressBook { name: a, .. },
DavResourceMetadata::AddressBook { name: b, .. },
) => a != b,
(
DavResourceMetadata::CalendarEvent { names: a, .. },
DavResourceMetadata::CalendarEvent { names: b, .. },
) => a != b,
(
DavResourceMetadata::ContactCard { names: a, .. },
DavResourceMetadata::ContactCard { names: b, .. },
) => a != b,
_ => unreachable!(),
}
}
pub fn event_time_range(&self) -> Option<(i64, i64)> {
match &self.data {
DavResourceMetadata::CalendarEvent { start, duration } => {
Some((*start, *start + *duration as i64))
}
DavResourceMetadata::CalendarEvent {
start, duration, ..
} => Some((*start, *start + *duration as i64)),
_ => None,
}
}
pub fn timezone(&self) -> Option<Tz> {
match &self.data {
DavResourceMetadata::Calendar { tz } => Some(*tz),
DavResourceMetadata::Calendar { tz, .. } => Some(*tz),
_ => None,
}
}
pub fn is_container(&self) -> bool {
match &self.data {
DavResourceMetadata::File { is_container, .. } => *is_container,
_ => self.parent_id.is_none(),
DavResourceMetadata::File { size, .. } => size.is_none(),
DavResourceMetadata::Calendar { .. } | DavResourceMetadata::AddressBook { .. } => true,
_ => false,
}
}
pub fn size(&self) -> u32 {
match &self.data {
DavResourceMetadata::File { size, .. } => *size,
DavResourceMetadata::File { size, .. } => size.unwrap_or_default(),
_ => 0,
}
}
pub fn hierarchy_sequence(&self) -> u32 {
pub fn acls(&self) -> Option<&[AclGrant]> {
match &self.data {
DavResourceMetadata::File {
hierarchy_sequence, ..
} => *hierarchy_sequence,
_ => {
if self.parent_id.is_none() {
0
} else {
1
}
}
DavResourceMetadata::File { acls, .. } => Some(acls.as_slice()),
DavResourceMetadata::Calendar { acls, .. } => Some(acls.as_slice()),
DavResourceMetadata::AddressBook { acls, .. } => Some(acls.as_slice()),
_ => None,
}
}
}
impl IdBimapItem for DavResource {
fn id(&self) -> &u32 {
&self.document_id
impl Hash for DavPath {
fn hash<H: Hasher>(&self, state: &mut H) {
self.path.hash(state);
}
}
fn name(&self) -> &str {
&self.name
impl PartialEq for DavPath {
fn eq(&self, other: &Self) -> bool {
self.path == other.path
}
}
impl Eq for DavPath {}
impl std::borrow::Borrow<str> for DavPath {
fn borrow(&self) -> &str {
&self.path
}
}
@@ -608,6 +774,12 @@ impl std::borrow::Borrow<u32> for DavResource {
}
}
impl DavName {
pub fn new(name: String, parent_id: u32) -> Self {
Self { name, parent_id }
}
}
impl MessageStoreCache {
pub fn assign_thread_id(&self, thread_name: &[u8], message_id: &[u8]) -> u32 {
let mut bytes = Vec::with_capacity(thread_name.len() + message_id.len());

View File

@@ -4,6 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::auth::AccessToken;
use jmap_proto::types::{
acl::Acl,
value::{AclGrant, ArchivedAclGrant},
@@ -11,10 +12,9 @@ use jmap_proto::types::{
use rkyv::vec::ArchivedVec;
use utils::map::bitmap::Bitmap;
use crate::auth::AccessToken;
pub mod acl;
pub mod document;
pub mod resources;
pub trait EffectiveAcl {
fn effective_acl(&self, access_token: &AccessToken) -> Bitmap<Acl>;

View File

@@ -0,0 +1,83 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{DavResources, auth::AccessToken};
use jmap_proto::types::acl::Acl;
use store::roaring::RoaringBitmap;
use utils::map::bitmap::Bitmap;
impl DavResources {
pub fn shared_containers(
&self,
access_token: &AccessToken,
check_acls: impl IntoIterator<Item = Acl>,
match_any: bool,
) -> RoaringBitmap {
let check_acls = Bitmap::<Acl>::from_iter(check_acls);
let mut document_ids = RoaringBitmap::new();
for resource in &self.resources {
if let Some(acls) = resource.acls() {
for acl in acls {
if access_token.is_member(acl.account_id) {
let mut grants = acl.grants;
grants.intersection(&check_acls);
if grants == check_acls || (match_any && !grants.is_empty()) {
document_ids.insert(resource.document_id);
}
}
}
}
}
document_ids
}
pub fn has_access_to_container(
&self,
access_token: &AccessToken,
document_id: u32,
check_acls: impl Into<Bitmap<Acl>>,
) -> bool {
let check_acls = check_acls.into();
for resource in &self.resources {
if resource.document_id == document_id {
if let Some(acls) = resource.acls() {
for acl in acls {
if access_token.is_member(acl.account_id) {
let mut grants = acl.grants;
grants.intersection(&check_acls);
return !grants.is_empty();
}
}
break;
}
}
}
false
}
pub fn container_acl(&self, access_token: &AccessToken, document_id: u32) -> Bitmap<Acl> {
let mut account_acls = Bitmap::<Acl>::new();
for resource in &self.resources {
if resource.document_id == document_id {
if let Some(acls) = resource.acls() {
for acl in acls {
if access_token.is_member(acl.account_id) {
account_acls.union(&acl.grants);
}
}
break;
}
}
}
account_acls
}
}

View File

@@ -1,217 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use ahash::AHashMap;
use jmap_proto::types::{collection::Collection, property::Property};
use store::{
Deserialize, IndexKey, IterateParams, SerializeInfallible, SerializedVersion, U32_LEN,
ValueKey,
write::{AlignedBytes, Archive, ValueClass, key::DeserializeBigEndian},
};
use trc::AddContext;
use utils::topological::{TopologicalSort, TopologicalSortIterator};
use crate::Server;
pub struct ExpandedFolders {
names: AHashMap<u32, ExpandedFolder>,
iter: TopologicalSortIterator<u32>,
}
#[derive(Debug, Clone)]
pub struct ExpandedFolder {
pub name: String,
pub document_id: u32,
pub parent_id: Option<u32>,
pub is_container: bool,
pub size: u32,
pub hierarchy_sequence: u32,
}
pub trait FolderHierarchy: Sync + Send {
fn name(&self) -> String;
fn parent_id(&self) -> u32;
fn is_container(&self) -> bool;
fn size(&self) -> u32;
}
pub trait TopologyBuilder: Sync + Send {
fn insert(&mut self, folder_id: u32, parent_id: u32);
}
impl Server {
pub async fn fetch_folders<T>(
&self,
account_id: u32,
collection: Collection,
) -> trc::Result<ExpandedFolders>
where
T: rkyv::Archive + SerializedVersion,
T::Archived: FolderHierarchy
+ for<'a> rkyv::bytecheck::CheckBytes<
rkyv::api::high::HighValidator<'a, rkyv::rancor::Error>,
> + rkyv::Deserialize<T, rkyv::api::high::HighDeserializer<rkyv::rancor::Error>>,
{
let collection_: u8 = collection.into();
let mut names = AHashMap::with_capacity(10);
let mut topological_sort = TopologicalSort::with_capacity(10);
self.core
.storage
.data
.iterate(
IterateParams::new(
ValueKey {
account_id,
collection: collection_,
document_id: 0,
class: ValueClass::Property(Property::Value.into()),
},
ValueKey {
account_id,
collection: collection_,
document_id: u32::MAX,
class: ValueClass::Property(Property::Value.into()),
},
),
|key, value| {
let document_id = key.deserialize_be_u32(key.len() - U32_LEN)?;
let archive = <Archive<AlignedBytes> as Deserialize>::deserialize(value)?;
let folder = archive.unarchive::<T>()?;
let parent_id = folder.parent_id();
topological_sort.insert(parent_id, document_id + 1);
names.insert(
document_id,
ExpandedFolder {
name: folder.name(),
document_id,
parent_id: if parent_id > 0 {
Some(parent_id - 1)
} else {
None
},
is_container: folder.is_container(),
size: folder.size(),
hierarchy_sequence: 0,
},
);
Ok(true)
},
)
.await
.add_context(|err| {
err.caused_by(trc::location!())
.account_id(account_id)
.collection(collection)
})?;
Ok(ExpandedFolders {
names,
iter: topological_sort.into_iterator(),
})
}
pub async fn fetch_folder_topology<T>(
&self,
account_id: u32,
collection: Collection,
topology: &mut impl TopologyBuilder,
) -> trc::Result<()>
where
T: TopologyBuilder,
{
self.store()
.iterate(
IterateParams::new(
IndexKey {
account_id,
collection: collection.into(),
document_id: 0,
field: Property::ParentId.into(),
key: 0u32.serialize(),
},
IndexKey {
account_id,
collection: collection.into(),
document_id: u32::MAX,
field: Property::ParentId.into(),
key: u32::MAX.serialize(),
},
)
.no_values()
.ascending(),
|key, _| {
let document_id = key
.get(key.len() - U32_LEN..)
.ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))
.and_then(u32::deserialize)?;
let parent_id = key
.get(key.len() - (U32_LEN * 2)..key.len() - U32_LEN)
.ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))
.and_then(u32::deserialize)?;
topology.insert(document_id, parent_id);
Ok(true)
},
)
.await
.caused_by(trc::location!())?;
Ok(())
}
}
impl ExpandedFolders {
pub fn len(&self) -> usize {
self.names.len()
}
pub fn is_empty(&self) -> bool {
self.names.is_empty()
}
pub fn format<T>(mut self, formatter: T) -> Self
where
T: Fn(&mut ExpandedFolder),
{
for folder in self.names.values_mut() {
formatter(folder);
}
self
}
pub fn into_iterator(mut self) -> impl Iterator<Item = ExpandedFolder> + Sync + Send {
for (hierarchy_sequence, folder_id) in self.iter.by_ref().enumerate() {
if folder_id != 0 {
let folder_id = folder_id - 1;
if let Some((name, parent_name)) = self
.names
.get(&folder_id)
.and_then(|folder| folder.parent_id.map(|parent_id| (&folder.name, parent_id)))
.and_then(|(name, parent_id)| {
self.names
.get(&parent_id)
.map(|folder| (name, &folder.name))
})
{
let name = format!("{parent_name}/{name}");
let folder = self.names.get_mut(&folder_id).unwrap();
folder.name = name;
folder.hierarchy_sequence = hierarchy_sequence as u32;
} else {
self.names.get_mut(&folder_id).unwrap().hierarchy_sequence =
hierarchy_sequence as u32;
}
}
}
self.names.into_values()
}
}

View File

@@ -5,6 +5,5 @@
*/
pub mod blob;
pub mod folder;
pub mod index;
pub mod state;