use std::error::Error;
use std::fmt::{self, Display, Formatter};
use serde::{Deserialize, Serialize};
use crate::{
DurabilityClass, ExternalKeyResolverCapabilityId, IndexMode, LifecyclePolicyId, ResourceRevision, StorageClass,
};
pub const INDEX_RESOURCE_SPEC_FORMAT_VERSION: u16 = 3;
pub const MAX_RESOURCE_INITIAL_SHARDS: u16 = 4_096;
pub const MAX_RESOURCE_BYTES: u64 = 1 << 50;
pub const MAX_RESOURCE_ADMISSION_WEIGHT: u32 = 1_000_000;
pub const MAX_RESOURCE_REFRESH_MILLIS: u32 = 7 * 24 * 60 * 60 * 1_000;
pub const MAX_RESOURCE_PARALLELISM: u16 = 4_096;
pub const MIN_RESOURCE_MEMTABLE_BYTES: u64 = 15_000_000;
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum VectorQualityObjectiveV1 {
#[default]
Balanced,
HighRecall,
MemoryEfficient,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum VectorIndexingObjectiveV1 {
FastBuild,
#[default]
Balanced,
Compact,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum VectorResidencyIntentV1 {
Automatic,
PreferResident,
RequireResident,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum VectorStrategyProfileV1 {
BalancedV1,
HighRecallV1,
MemoryEfficientV1,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct VectorCalibrationBindingV1 {
pub profile: VectorStrategyProfileV1,
pub evidence_digest: [u8; 32],
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct VectorResourcePolicyV1 {
pub quality: VectorQualityObjectiveV1,
pub indexing: VectorIndexingObjectiveV1,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub residency: Option<VectorResidencyIntentV1>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub calibration: Option<VectorCalibrationBindingV1>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct IndexResourceSpecV1 {
pub format_version: u16,
pub revision: ResourceRevision,
pub initial_shards: u16,
pub target_shard_bytes: u64,
pub durability: DurabilityClass,
pub storage_class: StorageClass,
pub ram_admission_weight: u32,
pub ram_hard_max_bytes: u64,
pub nvme_admission_weight: u32,
pub nvme_hard_max_bytes: u64,
pub refresh_objective_millis: u32,
pub memtable_hard_max_bytes: u64,
pub seal_parallelism: u16,
pub merge_parallelism: u16,
pub vector: VectorResourcePolicyV1,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lifecycle_binding: Option<LifecyclePolicyId>,
pub encryption_required: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resolver_capability: Option<ExternalKeyResolverCapabilityId>,
}
impl IndexResourceSpecV1 {
pub fn validate(self) -> Result<(), IndexResourceSpecError> {
if self.format_version != INDEX_RESOURCE_SPEC_FORMAT_VERSION {
return Err(IndexResourceSpecError::UnsupportedVersion {
observed: self.format_version,
supported: INDEX_RESOURCE_SPEC_FORMAT_VERSION,
});
}
if self.revision.get() == 0 {
return Err(IndexResourceSpecError::Invalid("revision must be nonzero"));
}
if self.initial_shards == 0 || self.initial_shards > MAX_RESOURCE_INITIAL_SHARDS {
return Err(IndexResourceSpecError::Invalid(
"initial_shards is outside the supported bound",
));
}
if self.target_shard_bytes == 0 || self.target_shard_bytes > MAX_RESOURCE_BYTES {
return Err(IndexResourceSpecError::Invalid(
"target_shard_bytes is outside the supported bound",
));
}
if self.ram_admission_weight == 0
|| self.ram_admission_weight > MAX_RESOURCE_ADMISSION_WEIGHT
|| self.nvme_admission_weight == 0
|| self.nvme_admission_weight > MAX_RESOURCE_ADMISSION_WEIGHT
{
return Err(IndexResourceSpecError::Invalid(
"RAM and NVMe admission weights are outside the supported bound",
));
}
if self.ram_hard_max_bytes == 0
|| self.ram_hard_max_bytes > MAX_RESOURCE_BYTES
|| self.nvme_hard_max_bytes == 0
|| self.nvme_hard_max_bytes > MAX_RESOURCE_BYTES
{
return Err(IndexResourceSpecError::Invalid(
"RAM and NVMe hard maxima are outside the supported bound",
));
}
if self.refresh_objective_millis == 0 || self.refresh_objective_millis > MAX_RESOURCE_REFRESH_MILLIS {
return Err(IndexResourceSpecError::Invalid(
"refresh_objective_millis is outside the supported bound",
));
}
if self.memtable_hard_max_bytes < MIN_RESOURCE_MEMTABLE_BYTES
|| self.memtable_hard_max_bytes > MAX_RESOURCE_BYTES
{
return Err(IndexResourceSpecError::Invalid(
"memtable_hard_max_bytes is outside the supported bound",
));
}
if self.seal_parallelism == 0
|| self.seal_parallelism > MAX_RESOURCE_PARALLELISM
|| self.merge_parallelism == 0
|| self.merge_parallelism > MAX_RESOURCE_PARALLELISM
{
return Err(IndexResourceSpecError::Invalid(
"seal and merge parallelism are outside the supported bound",
));
}
if self
.vector
.calibration
.is_some_and(|binding| binding.evidence_digest == [0; 32])
{
return Err(IndexResourceSpecError::Invalid(
"vector calibration evidence digest cannot be zero",
));
}
if self.lifecycle_binding.is_some_and(|binding| binding.get() == 0) {
return Err(IndexResourceSpecError::Invalid("lifecycle binding must be nonzero"));
}
if self.encryption_required != self.resolver_capability.is_some()
|| self.resolver_capability.is_some_and(|capability| capability.get() == 0)
{
return Err(IndexResourceSpecError::Invalid(
"encryption requires one nonzero resolver capability and an unencrypted index carries none",
));
}
Ok(())
}
pub fn validate_current_execution(self, mode: IndexMode) -> Result<(), IndexResourceSpecError> {
self.validate()?;
let durability = if mode == IndexMode::State {
self.durability.require_state_available()
} else {
self.durability.require_available()
};
durability.map_err(|_| IndexResourceSpecError::Unavailable("durability class"))?;
if self.storage_class != StorageClass::Standard {
return Err(IndexResourceSpecError::Unavailable("storage class"));
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IndexResourceSpecError {
UnsupportedVersion { observed: u16, supported: u16 },
Invalid(&'static str),
Unavailable(&'static str),
}
impl Display for IndexResourceSpecError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::UnsupportedVersion { observed, supported } => {
write!(
formatter,
"unsupported resource-spec version {observed}; supported version is {supported}"
)
}
Self::Invalid(reason) => write!(formatter, "invalid index resource specification: {reason}"),
Self::Unavailable(axis) => write!(formatter, "index resource specification requests an unavailable {axis}"),
}
}
}
impl Error for IndexResourceSpecError {}
#[cfg(test)]
mod tests {
use super::*;
fn valid() -> IndexResourceSpecV1 {
IndexResourceSpecV1 {
format_version: INDEX_RESOURCE_SPEC_FORMAT_VERSION,
revision: ResourceRevision::new(1),
initial_shards: 2,
target_shard_bytes: 64 << 20,
durability: DurabilityClass::RegionalObject,
storage_class: StorageClass::Standard,
ram_admission_weight: 3,
ram_hard_max_bytes: 128 << 20,
nvme_admission_weight: 2,
nvme_hard_max_bytes: 512 << 20,
refresh_objective_millis: 1_000,
memtable_hard_max_bytes: 32 << 20,
seal_parallelism: 2,
merge_parallelism: 1,
vector: VectorResourcePolicyV1::default(),
lifecycle_binding: None,
encryption_required: false,
resolver_capability: None,
}
}
#[test]
fn validates_current_operational_values_without_product_tiers() {
assert_eq!(valid().validate_current_execution(IndexMode::Mutable), Ok(()));
let encoded = serde_json::to_string(&valid());
assert!(encoded.is_ok());
let encoded = encoded.unwrap_or_default();
for forbidden in ["free", "growth", "enterprise", "price", "tenant"] {
assert!(!encoded.contains(forbidden));
}
}
#[test]
fn vector_residency_is_an_optional_performance_intent() {
let mut spec = valid();
spec.vector.residency = Some(VectorResidencyIntentV1::RequireResident);
assert_eq!(spec.validate_current_execution(IndexMode::Mutable), Ok(()));
let encoded = serde_json::to_string(&spec).unwrap_or_default();
assert!(encoded.contains("\"residency\":\"require_resident\""));
assert!(!encoded.contains("warm"));
}
#[test]
fn refuses_newer_versions_and_unbound_encryption() {
let mut newer = valid();
newer.format_version = INDEX_RESOURCE_SPEC_FORMAT_VERSION + 1;
assert!(matches!(
newer.validate(),
Err(IndexResourceSpecError::UnsupportedVersion { .. })
));
let mut unbound = valid();
unbound.encryption_required = true;
assert!(matches!(unbound.validate(), Err(IndexResourceSpecError::Invalid(_))));
let mut zero_evidence = valid();
zero_evidence.vector.calibration = Some(VectorCalibrationBindingV1 {
profile: VectorStrategyProfileV1::BalancedV1,
evidence_digest: [0; 32],
});
assert!(matches!(
zero_evidence.validate(),
Err(IndexResourceSpecError::Invalid(_))
));
}
}