use std::num::NonZeroUsize;
use foundationdb::{
RangeOption, Transaction,
future::{FdbSlice, FdbValues},
options::MutationType,
};
use thiserror::Error;
const RESERVED_METADATA_START: &[u8] = b"__meta";
const RESERVED_METADATA_END: &[u8] = b"__metb";
const MAX_TENANT_KEY_BYTES: usize = 10_000;
const MAX_TENANT_VALUE_BYTES: usize = 100_000;
const MAX_RANGE_RESULTS: usize = 64;
const MAX_RANGE_TARGET_BYTES: usize = 1_000_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum TenantDataAccessErrorReason {
#[error("tenant data key is empty")]
EmptyKey,
#[error("tenant data key exceeds the size limit")]
KeyTooLong,
#[error("tenant data key is reserved")]
ReservedKey,
#[error("tenant data value exceeds the size limit")]
ValueTooLong,
#[error("tenant data range bounds are invalid")]
InvalidRange,
#[error("tenant data range overlaps reserved metadata")]
ReservedRange,
#[error("tenant data range limit is invalid")]
InvalidRangeLimit,
#[error("tenant data mutation changes the validated key")]
KeyChangingMutation,
}
#[derive(Clone, Copy)]
pub struct TenantDataKey<'a> {
bytes: &'a [u8],
}
impl<'a> TenantDataKey<'a> {
pub fn new(bytes: &'a [u8]) -> Result<Self, TenantDataAccessErrorReason> {
if bytes.is_empty() {
return Err(TenantDataAccessErrorReason::EmptyKey);
}
if bytes.len() > MAX_TENANT_KEY_BYTES {
return Err(TenantDataAccessErrorReason::KeyTooLong);
}
if (RESERVED_METADATA_START..RESERVED_METADATA_END).contains(&bytes) || bytes[0] == 0xff {
return Err(TenantDataAccessErrorReason::ReservedKey);
}
Ok(Self { bytes })
}
pub fn as_bytes(self) -> &'a [u8] {
self.bytes
}
}
pub struct TenantDataRange<'a> {
begin: TenantDataKey<'a>,
end: TenantDataKey<'a>,
}
impl<'a> TenantDataRange<'a> {
pub fn new(begin: &'a [u8], end: &'a [u8]) -> Result<Self, TenantDataAccessErrorReason> {
let begin = TenantDataKey::new(begin)?;
let end = TenantDataKey::new(end)?;
if begin.as_bytes() >= end.as_bytes() {
return Err(TenantDataAccessErrorReason::InvalidRange);
}
if begin.as_bytes() < RESERVED_METADATA_END && end.as_bytes() > RESERVED_METADATA_START {
return Err(TenantDataAccessErrorReason::ReservedRange);
}
Ok(Self { begin, end })
}
pub fn begin(&self) -> &[u8] {
self.begin.as_bytes()
}
pub fn end(&self) -> &[u8] {
self.end.as_bytes()
}
}
#[derive(Clone, Copy)]
pub struct TenantDataRangeLimit(NonZeroUsize);
impl TenantDataRangeLimit {
pub fn new(value: usize) -> Result<Self, TenantDataAccessErrorReason> {
let value = NonZeroUsize::new(value)
.filter(|value| value.get() <= MAX_RANGE_RESULTS)
.ok_or(TenantDataAccessErrorReason::InvalidRangeLimit)?;
Ok(Self(value))
}
pub fn get(self) -> usize {
self.0.get()
}
}
pub struct TenantDataTransaction<'a> {
inner: &'a Transaction,
}
impl<'a> TenantDataTransaction<'a> {
pub(super) const fn new(inner: &'a Transaction) -> Self {
Self { inner }
}
pub async fn get(
&self,
key: TenantDataKey<'_>,
snapshot: bool,
) -> foundationdb::FdbResult<Option<FdbSlice>> {
self.inner.get(key.as_bytes(), snapshot).await
}
pub fn set(
&self,
key: TenantDataKey<'_>,
value: &[u8],
) -> Result<(), TenantDataAccessErrorReason> {
if value.len() > MAX_TENANT_VALUE_BYTES {
return Err(TenantDataAccessErrorReason::ValueTooLong);
}
self.inner.set(key.as_bytes(), value);
Ok(())
}
pub fn clear(&self, key: TenantDataKey<'_>) {
self.inner.clear(key.as_bytes());
}
pub fn clear_range(&self, range: &TenantDataRange<'_>) {
self.inner.clear_range(range.begin(), range.end());
}
pub fn atomic_op(
&self,
key: TenantDataKey<'_>,
parameter: &[u8],
mutation: MutationType,
) -> Result<(), TenantDataAccessErrorReason> {
validate_atomic_mutation(mutation)?;
if parameter.len() > MAX_TENANT_VALUE_BYTES {
return Err(TenantDataAccessErrorReason::ValueTooLong);
}
self.inner.atomic_op(key.as_bytes(), parameter, mutation);
Ok(())
}
pub async fn get_range(
&self,
range: &TenantDataRange<'_>,
limit: TenantDataRangeLimit,
snapshot: bool,
) -> foundationdb::FdbResult<FdbValues> {
let mut options = RangeOption::from((range.begin(), range.end()));
options.limit = Some(limit.get());
options.target_bytes = MAX_RANGE_TARGET_BYTES;
self.inner.get_range(&options, 1, snapshot).await
}
}
fn validate_atomic_mutation(mutation: MutationType) -> Result<(), TenantDataAccessErrorReason> {
match mutation {
MutationType::Add
| MutationType::And
| MutationType::BitAnd
| MutationType::Or
| MutationType::BitOr
| MutationType::Xor
| MutationType::BitXor
| MutationType::AppendIfFits
| MutationType::Max
| MutationType::Min
| MutationType::SetVersionstampedValue
| MutationType::ByteMin
| MutationType::ByteMax
| MutationType::CompareAndClear => Ok(()),
MutationType::SetVersionstampedKey => Err(TenantDataAccessErrorReason::KeyChangingMutation),
_ => Err(TenantDataAccessErrorReason::KeyChangingMutation),
}
}
#[cfg(test)]
#[path = "data_tests.rs"]
mod tests;