CardDAV skeleton + Message ingestion performance improvements

This commit is contained in:
mdecimus
2025-03-28 17:25:30 +01:00
parent a5e6f77b26
commit 372f2bec70
43 changed files with 1007 additions and 302 deletions

View File

@@ -1,6 +1,6 @@
[package]
name = "groupware"
version = "0.11.5"
version = "0.11.7"
edition = "2024"
resolver = "2"
@@ -12,7 +12,7 @@ jmap_proto = { path = "../jmap-proto" }
trc = { path = "../trc" }
directory = { path = "../directory" }
dav-proto = { path = "../dav-proto" }
calcard = { path = "/Users/me/code/calcard" }
calcard = { path = "/Users/me/code/calcard", features = ["rkyv"] }
hashify = "0.2"
rkyv = { version = "0.8.10", features = ["little_endian"] }
percent-encoding = "2.3.1"

View File

@@ -28,12 +28,15 @@ pub struct CalendarPreferences {
pub is_subscribed: bool,
pub is_default: bool,
pub is_visible: bool,
/*pub include_in_availability: IncludeInAvailability,
pub include_in_availability: IncludeInAvailability,
pub default_alerts_with_time: VecMap<String, ICalendar>,
pub default_alerts_without_time: VecMap<String, ICalendar>,
pub time_zone: Timezone,*/
pub time_zone: Timezone,
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
pub struct CalendarEvent {
pub name: Option<String>,
pub event: ICalendar,
@@ -47,15 +50,24 @@ pub struct CalendarEvent {
pub is_draft: bool,
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
pub enum Timezone {
IANA(String),
Custom(ICalendar),
#[default]
Default,
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
#[rkyv(derive(Debug))]
pub enum IncludeInAvailability {
All,
Attending,
#[default]
None,
}

View File

@@ -0,0 +1,81 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::storage::index::{IndexValue, IndexableAndSerializableObject, IndexableObject};
use jmap_proto::types::{property::Property, value::AclGrant};
use super::{AddressBook, ArchivedAddressBook, ContactCard};
impl IndexableObject for AddressBook {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
[
IndexValue::Text {
field: Property::Name.into(),
value: self.name.as_str().into(),
},
IndexValue::Acl {
value: (&self.acls).into(),
},
IndexValue::Quota {
used: self.dead_properties.size() as u32
+ self.display_name.as_ref().map_or(0, |n| n.len() as u32)
+ self.description.as_ref().map_or(0, |n| n.len() as u32)
+ self.name.len() as u32,
},
]
.into_iter()
}
}
impl IndexableObject for &ArchivedAddressBook {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
[
IndexValue::Text {
field: Property::Name.into(),
value: self.name.as_str().into(),
},
IndexValue::Acl {
value: self
.acls
.iter()
.map(AclGrant::from)
.collect::<Vec<_>>()
.into(),
},
IndexValue::Quota {
used: self.dead_properties.size() as u32
+ self.display_name.as_ref().map_or(0, |n| n.len() as u32)
+ self.description.as_ref().map_or(0, |n| n.len() as u32)
+ self.name.len() as u32,
},
]
.into_iter()
}
}
impl IndexableAndSerializableObject for AddressBook {}
impl IndexableObject for ContactCard {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
[
IndexValue::Text {
field: Property::Name.into(),
value: self.name.as_str().into(),
},
IndexValue::U32List {
field: Property::ParentId.into(),
value: self.addressbook_ids.as_slice().into(),
},
IndexValue::Quota {
used: self.dead_properties.size() as u32
+ self.display_name.as_ref().map_or(0, |n| n.len() as u32)
+ self.name.len() as u32
+ self.size,
},
]
.into_iter()
}
}

View File

@@ -4,7 +4,10 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod index;
use calcard::vcard::VCard;
use dav_proto::schema::request::DeadProperty;
use jmap_proto::types::{acl::Acl, value::AclGrant};
use store::{SERIALIZE_OBJ_15_V1, SerializedVersion};
@@ -19,6 +22,7 @@ pub struct AddressBook {
pub sort_order: u32,
pub is_default: bool,
pub subscribers: Vec<u32>,
pub dead_properties: DeadProperty,
pub acls: Vec<AclGrant>,
}
@@ -29,13 +33,19 @@ pub enum AddressBookRight {
Delete,
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
#[rkyv(derive(Debug))]
pub struct ContactCard {
pub name: Option<String>,
pub name: String,
pub display_name: Option<String>,
pub addressbook_ids: Vec<u32>,
pub card: VCard,
pub dead_properties: DeadProperty,
pub created: u64,
pub updated: u64,
pub size: u32,
}
impl TryFrom<Acl> for AddressBookRight {

View File

@@ -1,74 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::sync::Arc;
use common::{FileItem, Files, Server};
use jmap_proto::types::collection::Collection;
use trc::AddContext;
use utils::bimap::IdBimap;
use crate::file::FileNode;
pub trait FileHierarchy: Sync + Send {
fn fetch_file_hierarchy(
&self,
account_id: u32,
) -> impl Future<Output = trc::Result<Arc<Files>>> + Send;
}
impl FileHierarchy for Server {
async fn fetch_file_hierarchy(&self, account_id: u32) -> trc::Result<Arc<Files>> {
let change_id = self
.store()
.get_last_change_id(account_id, Collection::FileNode)
.await
.caused_by(trc::location!())?;
if let Some(files) = self
.inner
.cache
.files
.get(&account_id)
.filter(|x| x.modseq == change_id)
{
Ok(files)
} else {
let mut files = build_file_hierarchy(self, account_id).await?;
files.modseq = change_id;
let files = Arc::new(files);
self.inner.cache.files.insert(account_id, files.clone());
Ok(files)
}
}
}
async fn build_file_hierarchy(server: &Server, account_id: u32) -> trc::Result<Files> {
let list = server
.fetch_folders::<FileNode>(account_id, Collection::FileNode)
.await
.caused_by(trc::location!())?;
let mut files = Files {
files: IdBimap::with_capacity(list.len()),
size: std::mem::size_of::<Files>() as u64,
modseq: None,
};
for expanded in list.into_iterator() {
files.size += (std::mem::size_of::<u32>()
+ std::mem::size_of::<String>()
+ expanded.name.len()) as u64;
files.files.insert(FileItem {
document_id: expanded.document_id,
parent_id: expanded.parent_id,
name: expanded.name,
size: expanded.size,
is_container: expanded.is_container,
hierarchy_sequence: expanded.hierarchy_sequence,
});
}
Ok(files)
}

View File

@@ -4,7 +4,6 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod hierarchy;
pub mod index;
use dav_proto::schema::request::DeadProperty;

View File

@@ -0,0 +1,209 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::sync::Arc;
use common::{DavResource, DavResourceId, DavResources, Server};
use jmap_proto::types::{collection::Collection, property::Property};
use store::{
Deserialize, IndexKey, IterateParams, SerializeInfallible, U32_LEN, ahash::AHashMap,
write::key::DeserializeBigEndian,
};
use trc::AddContext;
use utils::bimap::IdBimap;
use crate::file::FileNode;
pub trait DavHierarchy: Sync + Send {
fn fetch_dav_hierarchy(
&self,
account_id: u32,
collection: Collection,
) -> impl Future<Output = trc::Result<Arc<DavResources>>> + Send;
}
impl DavHierarchy for Server {
async fn fetch_dav_hierarchy(
&self,
account_id: u32,
collection: Collection,
) -> trc::Result<Arc<DavResources>> {
let change_id = self
.store()
.get_last_change_id(account_id, collection)
.await
.caused_by(trc::location!())?;
let resource_id = DavResourceId {
account_id,
collection: collection.into(),
};
if let Some(files) = self
.inner
.cache
.dav
.get(&resource_id)
.filter(|x| x.modseq == change_id)
{
Ok(files)
} else {
let mut files = match collection {
Collection::Calendar | Collection::AddressBook => {
build_hierarchy(self, account_id, collection).await?
}
Collection::FileNode => build_file_hierarchy(self, account_id).await?,
_ => unreachable!(),
};
files.modseq = change_id;
let files = Arc::new(files);
self.inner.cache.dav.insert(resource_id, files.clone());
Ok(files)
}
}
}
#[derive(Default)]
struct DavTempResource {
name: String,
parent_id: Vec<u32>,
}
async fn build_hierarchy(
server: &Server,
account_id: u32,
collection: Collection,
) -> trc::Result<DavResources> {
let collection = u8::from(collection);
let mut containers: AHashMap<u32, DavTempResource> = AHashMap::with_capacity(16);
let mut resources: AHashMap<u32, DavTempResource> = AHashMap::with_capacity(16);
server
.store()
.iterate(
IterateParams::new(
IndexKey {
account_id,
collection,
document_id: 0,
field: 0,
key: 0u32.serialize(),
},
IndexKey {
account_id,
collection: collection + 1,
document_id: u32::MAX,
field: u8::MAX,
key: u32::MAX.serialize(),
},
)
.no_values()
.ascending(),
|key, _| {
let document_id = key.deserialize_be_u32(key.len() - U32_LEN)?;
let value = key
.get(key.len() - (U32_LEN * 2)..key.len() - U32_LEN)
.ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?;
let key_collection = key
.get(U32_LEN)
.copied()
.ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?;
let key_property = key
.get(U32_LEN + 1)
.copied()
.ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?;
let resource = if key_collection == collection {
containers.entry(document_id).or_default()
} else {
resources.entry(document_id).or_default()
};
if key_property == u8::from(Property::Value) {
resource.name = std::str::from_utf8(value)
.map_err(|_| trc::Error::corrupted_key(key, None, trc::location!()))?
.to_string();
} else if key_property == u8::from(Property::ParentId) {
resource.parent_id.push(
u32::deserialize(value)
.map_err(|_| trc::Error::corrupted_key(key, None, trc::location!()))?,
);
}
Ok(true)
},
)
.await
.caused_by(trc::location!())?;
let mut files = DavResources {
files: IdBimap::with_capacity(containers.len() + resources.len()),
size: std::mem::size_of::<DavResources>() as u64,
modseq: None,
};
for (document_id, resource) in resources {
for parent_id in resource.parent_id {
if let Some(container) = containers.get(&parent_id) {
let name = format!("{}/{}", container.name, resource.name);
files.size += (std::mem::size_of::<u32>()
+ std::mem::size_of::<String>()
+ name.len()) as u64;
files.files.insert(DavResource {
document_id,
parent_id: parent_id.into(),
name,
size: 0,
is_container: false,
hierarchy_sequence: 1,
});
}
}
}
for (document_id, container) in containers {
files.size += (std::mem::size_of::<u32>()
+ std::mem::size_of::<String>()
+ container.name.len()) as u64;
files.files.insert(DavResource {
document_id,
parent_id: None,
name: container.name,
size: 0,
is_container: true,
hierarchy_sequence: 0,
});
}
Ok(files)
}
async fn build_file_hierarchy(server: &Server, account_id: u32) -> trc::Result<DavResources> {
let list = server
.fetch_folders::<FileNode>(account_id, Collection::FileNode)
.await
.caused_by(trc::location!())?;
let mut files = DavResources {
files: IdBimap::with_capacity(list.len()),
size: std::mem::size_of::<DavResources>() as u64,
modseq: None,
};
for expanded in list.into_iterator() {
files.size += (std::mem::size_of::<u32>()
+ std::mem::size_of::<String>()
+ expanded.name.len()) as u64;
files.files.insert(DavResource {
document_id: expanded.document_id,
parent_id: expanded.parent_id,
name: expanded.name,
size: expanded.size,
is_container: expanded.is_container,
hierarchy_sequence: expanded.hierarchy_sequence,
});
}
Ok(files)
}

View File

@@ -7,3 +7,4 @@
pub mod calendar;
pub mod contact;
pub mod file;
pub mod hierarchy;