1use crate::doc::{Doc, DocumentMap};
4use crate::error::{Error, Result};
5use crate::index::IndexRegistry;
6use crate::schema::CollectionSchema;
7use serde::{Deserialize, Deserializer, Serialize};
8use std::collections::BTreeSet;
9use std::sync::Arc;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Default)]
18pub struct CollectionResourceLimits {
19 #[serde(rename = "max_documents")]
20 documents: Option<u64>,
21 #[serde(rename = "max_accounted_bytes")]
22 accounted_bytes: Option<u64>,
23 #[serde(rename = "max_query_candidates")]
24 query_candidates: Option<u64>,
25 #[serde(rename = "max_write_batch_documents")]
26 write_batch_documents: Option<u64>,
27}
28
29#[derive(Deserialize, Default)]
30#[serde(default, deny_unknown_fields)]
31struct CollectionResourceLimitsWire {
32 #[serde(rename = "max_documents")]
33 documents: Option<u64>,
34 #[serde(rename = "max_accounted_bytes")]
35 accounted_bytes: Option<u64>,
36 #[serde(rename = "max_query_candidates")]
37 query_candidates: Option<u64>,
38 #[serde(rename = "max_write_batch_documents")]
39 write_batch_documents: Option<u64>,
40}
41
42impl<'de> Deserialize<'de> for CollectionResourceLimits {
43 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
44 where
45 D: Deserializer<'de>,
46 {
47 let wire = CollectionResourceLimitsWire::deserialize(deserializer)?;
48 let mut limits = Self::new();
49 if let Some(limit) = wire.documents {
50 limits = limits
51 .try_with_max_documents(limit)
52 .map_err(serde::de::Error::custom)?;
53 }
54 if let Some(limit) = wire.accounted_bytes {
55 limits = limits
56 .try_with_max_accounted_bytes(limit)
57 .map_err(serde::de::Error::custom)?;
58 }
59 if let Some(limit) = wire.query_candidates {
60 limits = limits
61 .try_with_max_query_candidates(limit)
62 .map_err(serde::de::Error::custom)?;
63 }
64 if let Some(limit) = wire.write_batch_documents {
65 limits = limits
66 .try_with_max_write_batch_documents(limit)
67 .map_err(serde::de::Error::custom)?;
68 }
69 Ok(limits)
70 }
71}
72
73impl CollectionResourceLimits {
74 pub fn new() -> Self {
76 Self::default()
77 }
78
79 pub fn try_with_max_documents(mut self, limit: u64) -> Result<Self> {
81 self.documents = Some(positive_limit(limit, "max_documents")?);
82 Ok(self)
83 }
84
85 pub fn try_with_max_accounted_bytes(mut self, limit: u64) -> Result<Self> {
87 self.accounted_bytes = Some(positive_limit(limit, "max_accounted_bytes")?);
88 Ok(self)
89 }
90
91 pub fn try_with_max_query_candidates(mut self, limit: u64) -> Result<Self> {
93 self.query_candidates = Some(positive_limit(limit, "max_query_candidates")?);
94 Ok(self)
95 }
96
97 pub fn try_with_max_write_batch_documents(mut self, limit: u64) -> Result<Self> {
99 self.write_batch_documents = Some(positive_limit(limit, "max_write_batch_documents")?);
100 Ok(self)
101 }
102
103 pub fn max_documents(self) -> Option<u64> {
105 self.documents
106 }
107
108 pub fn max_accounted_bytes(self) -> Option<u64> {
110 self.accounted_bytes
111 }
112
113 pub fn max_query_candidates(self) -> Option<u64> {
115 self.query_candidates
116 }
117
118 pub fn max_write_batch_documents(self) -> Option<u64> {
120 self.write_batch_documents
121 }
122
123 pub(super) fn enforce_write_batch(self, documents: usize) -> Result<()> {
124 let documents = u64::try_from(documents)
125 .map_err(|_| Error::resource_exhausted("write batch exceeds u64 documents"))?;
126 if self
127 .write_batch_documents
128 .is_some_and(|limit| documents > limit)
129 {
130 return Err(Error::resource_exhausted(format!(
131 "write batch document count {documents} exceeds configured limit {}",
132 self.write_batch_documents.unwrap_or(u64::MAX)
133 )));
134 }
135 Ok(())
136 }
137
138 pub(super) fn enforce_query_candidates(self, candidates: u64) -> Result<()> {
139 if self
140 .query_candidates
141 .is_some_and(|limit| candidates > limit)
142 {
143 return Err(Error::resource_exhausted(format!(
144 "query candidate count {candidates} exceeds configured limit {}",
145 self.query_candidates.unwrap_or(u64::MAX)
146 )));
147 }
148 Ok(())
149 }
150
151 pub(super) fn enforce_state(
152 self,
153 schema: &CollectionSchema,
154 docs: &DocumentMap,
155 indexes: &IndexRegistry,
156 ) -> Result<ResourceUsage> {
157 let document_count = u64::try_from(docs.len())
158 .map_err(|_| Error::resource_exhausted("collection exceeds u64 documents"))?;
159 let usage = ResourceUsage::measure(schema, docs, indexes)?;
160 self.admit(document_count, usage)
161 }
162
163 pub(super) fn admit(self, document_count: u64, usage: ResourceUsage) -> Result<ResourceUsage> {
164 if self.documents.is_some_and(|limit| document_count > limit) {
165 return Err(Error::resource_exhausted(format!(
166 "collection document count {document_count} exceeds configured limit {}",
167 self.documents.unwrap_or(u64::MAX)
168 )));
169 }
170 if self
171 .accounted_bytes
172 .is_some_and(|limit| usage.total > limit)
173 {
174 return Err(Error::resource_exhausted(format!(
175 "collection accounted bytes {} exceeds configured limit {}",
176 usage.total,
177 self.accounted_bytes.unwrap_or(u64::MAX)
178 )));
179 }
180 Ok(usage)
181 }
182}
183
184fn positive_limit(limit: u64, name: &str) -> Result<u64> {
185 if limit == 0 {
186 Err(Error::invalid_argument(format!(
187 "resource limit '{name}' must be positive"
188 )))
189 } else {
190 Ok(limit)
191 }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub(super) struct ResourceUsage {
196 pub documents: u64,
197 pub indexes: u64,
198 pub total: u64,
199}
200
201impl ResourceUsage {
202 pub(super) fn measure(
203 _schema: &CollectionSchema,
204 docs: &DocumentMap,
205 indexes: &IndexRegistry,
206 ) -> Result<Self> {
207 Ok(Self::from_parts(
208 document_map_bytes(docs)?,
209 indexes.accounted_payload_bytes(),
210 ))
211 }
212
213 pub(super) fn after_documents(
221 &self,
222 previous_docs: &DocumentMap,
223 next_docs: &DocumentMap,
224 changed_ids: &BTreeSet<String>,
225 indexes: &IndexRegistry,
226 ) -> Result<Self> {
227 let mut documents = self.documents;
228 for id in changed_ids {
229 let before = document_entry_bytes(id, previous_docs.get(id))?;
230 let after = document_entry_bytes(id, next_docs.get(id))?;
231 if before > documents {
232 return Ok(Self::from_parts(
233 document_map_bytes(next_docs)?,
234 indexes.accounted_payload_bytes(),
235 ));
236 }
237 documents -= before;
238 documents = match documents.checked_add(after) {
239 Some(documents) => documents,
240 None => {
241 return Ok(Self::from_parts(
242 document_map_bytes(next_docs)?,
243 indexes.accounted_payload_bytes(),
244 ));
245 }
246 };
247 }
248 Ok(Self::from_parts(
249 documents,
250 indexes.accounted_payload_bytes(),
251 ))
252 }
253
254 fn from_parts(documents: u64, indexes: u64) -> Self {
255 Self {
256 documents,
257 indexes,
258 total: documents.saturating_add(indexes),
259 }
260 }
261}
262
263fn document_map_bytes(docs: &DocumentMap) -> Result<u64> {
264 bincode::serialized_size(docs)
265 .map_err(|error| Error::internal(format!("account document bytes: {error}")))
266}
267
268fn document_entry_bytes(id: &str, doc: Option<&Arc<Doc>>) -> Result<u64> {
269 let Some(doc) = doc else {
270 return Ok(0);
271 };
272 let key = bincode::serialized_size(id)
273 .map_err(|error| Error::internal(format!("account document key: {error}")))?;
274 let value = bincode::serialized_size(doc)
275 .map_err(|error| Error::internal(format!("account document value: {error}")))?;
276 Ok(key.saturating_add(value))
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn defaults_are_explicitly_unbounded() {
285 let limits = CollectionResourceLimits::default();
286 assert_eq!(limits.max_documents(), None);
287 assert_eq!(limits.max_accounted_bytes(), None);
288 assert_eq!(limits.max_query_candidates(), None);
289 assert_eq!(limits.max_write_batch_documents(), None);
290 }
291
292 #[test]
293 fn deserialization_preserves_the_typed_invariants() {
294 let limits: CollectionResourceLimits =
295 serde_json::from_str(r#"{"max_documents":10,"max_query_candidates":20}"#)
296 .expect("partial policies must default omitted limits to unbounded");
297 assert_eq!(limits.max_documents(), Some(10));
298 assert_eq!(limits.max_accounted_bytes(), None);
299 assert_eq!(limits.max_query_candidates(), Some(20));
300 assert!(
301 serde_json::from_str::<CollectionResourceLimits>(r#"{"max_documents":0}"#).is_err()
302 );
303 assert!(
304 serde_json::from_str::<CollectionResourceLimits>(r#"{"max_documentz":10}"#).is_err()
305 );
306 }
307
308 #[test]
309 fn public_policy_is_send_and_sync() {
310 fn assert_send_sync<T: Send + Sync>() {}
311 assert_send_sync::<CollectionResourceLimits>();
312 }
313
314 #[test]
315 fn document_map_bincode_size_is_the_header_plus_entries() {
316 use crate::doc::{Doc, DocumentMap};
317 use std::sync::Arc;
318
319 let mut docs = DocumentMap::new();
320 let mut running = bincode::serialized_size(&docs).expect("empty map size");
321 for index in 0..6 {
322 let mut doc = Doc::with_pk(format!("doc-{index}")).expect("primary key");
323 let coordinate = f32::from(u8::try_from(index).expect("fixture index"));
324 doc.add_vector_f32("embedding", &[coordinate, -1.0, 0.25])
325 .expect("vector");
326 if index % 2 == 0 {
327 doc.add_string("body", "kept").expect("field");
328 }
329 let id = doc.get_pk().expect("pk").to_string();
330 let stored = Arc::new(doc);
331 running += super::document_entry_bytes(&id, Some(&stored)).expect("entry");
332 docs.insert(id, stored);
333 assert_eq!(
334 bincode::serialized_size(&docs).expect("map size"),
335 running,
336 "insert {index}"
337 );
338 }
339 let removed_id = "doc-2".to_string();
340 let removed = docs.get(&removed_id).expect("present").clone();
341 running -= super::document_entry_bytes(&removed_id, Some(&removed)).expect("removed entry");
342 docs.remove(&removed_id);
343 assert_eq!(bincode::serialized_size(&docs).expect("map size"), running);
344
345 let replaced_id = "doc-4".to_string();
346 let previous = docs.get(&replaced_id).expect("present").clone();
347 let mut replacement = Doc::with_pk(replaced_id.clone()).expect("primary key");
348 replacement
349 .add_vector_f32("embedding", &[9.0, 8.0, 7.0])
350 .expect("vector");
351 let stored = Arc::new(replacement);
352 running -= super::document_entry_bytes(&replaced_id, Some(&previous)).expect("old entry");
353 running += super::document_entry_bytes(&replaced_id, Some(&stored)).expect("new entry");
354 docs.insert(replaced_id, stored);
355 assert_eq!(bincode::serialized_size(&docs).expect("map size"), running);
356 }
357}