/build/source/nativelink-store/src/mongo_store.rs
Line | Count | Source |
1 | | // Copyright 2025 The NativeLink Authors. All rights reserved. |
2 | | // |
3 | | // Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License"); |
4 | | // you may not use this file except in compliance with the License. |
5 | | // You may obtain a copy of the License at |
6 | | // |
7 | | // See LICENSE file for details |
8 | | // |
9 | | // Unless required by applicable law or agreed to in writing, software |
10 | | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | | // See the License for the specific language governing permissions and |
13 | | // limitations under the License. |
14 | | |
15 | | use core::cmp; |
16 | | use core::ops::Bound; |
17 | | use core::pin::Pin; |
18 | | use core::sync::atomic::{AtomicUsize, Ordering}; |
19 | | use core::time::Duration; |
20 | | use std::borrow::Cow; |
21 | | use std::sync::{Arc, Weak}; |
22 | | |
23 | | use async_trait::async_trait; |
24 | | use bytes::Bytes; |
25 | | use futures::stream::{Stream, StreamExt, TryStreamExt}; |
26 | | use mongodb::bson::{Bson, Document, doc}; |
27 | | use mongodb::options::{ClientOptions, FindOptions, IndexOptions, ReturnDocument, WriteConcern}; |
28 | | use mongodb::{Client as MongoClient, Collection, Database, IndexModel}; |
29 | | use nativelink_config::stores::ExperimentalMongoSpec; |
30 | | use nativelink_error::{Code, Error, make_err, make_input_err}; |
31 | | use nativelink_metric::MetricsComponent; |
32 | | use nativelink_util::buf_channel::{DropCloserReadHalf, DropCloserWriteHalf}; |
33 | | use nativelink_util::health_utils::{HealthRegistryBuilder, HealthStatus, HealthStatusIndicator}; |
34 | | use nativelink_util::spawn; |
35 | | use nativelink_util::store_trait::{ |
36 | | BoolValue, RemoveCallback, SchedulerCurrentVersionProvider, SchedulerIndexProvider, |
37 | | SchedulerStore, SchedulerStoreDataProvider, SchedulerStoreDecodeTo, SchedulerStoreKeyProvider, |
38 | | SchedulerSubscription, SchedulerSubscriptionManager, StoreDriver, StoreKey, UploadSizeInfo, |
39 | | }; |
40 | | use nativelink_util::task::JoinHandleDropGuard; |
41 | | use parking_lot::{Mutex, RwLock}; |
42 | | use patricia_tree::StringPatriciaMap; |
43 | | use tokio::sync::{Semaphore, SemaphorePermit, watch}; |
44 | | use tokio::time::sleep; |
45 | | use tracing::{error, info, trace, warn}; |
46 | | |
47 | | use crate::cas_utils::is_zero_digest; |
48 | | |
49 | | /// The default database name if not specified. |
50 | | const DEFAULT_DB_NAME: &str = "nativelink"; |
51 | | |
52 | | /// The default collection name if not specified. |
53 | | const DEFAULT_COLLECTION_NAME: &str = "cas"; |
54 | | |
55 | | /// The default scheduler collection name if not specified. |
56 | | const DEFAULT_SCHEDULER_COLLECTION_NAME: &str = "scheduler"; |
57 | | |
58 | | /// The default size of the read chunk when reading data from `MongoDB`. |
59 | | const DEFAULT_READ_CHUNK_SIZE: usize = 64 * 1024; |
60 | | |
61 | | /// The default connection timeout in milliseconds if not specified. |
62 | | const DEFAULT_CONNECTION_TIMEOUT_MS: u64 = 3000; |
63 | | |
64 | | /// The default command timeout in milliseconds if not specified. |
65 | | const DEFAULT_COMMAND_TIMEOUT_MS: u64 = 10_000; |
66 | | |
67 | | /// The name of the field in `MongoDB` documents that stores the key. |
68 | | const KEY_FIELD: &str = "_id"; |
69 | | |
70 | | /// The name of the field in `MongoDB` documents that stores the data. |
71 | | const DATA_FIELD: &str = "data"; |
72 | | |
73 | | /// The name of the field in `MongoDB` documents that stores the version. |
74 | | const VERSION_FIELD: &str = "version"; |
75 | | |
76 | | /// The name of the field in `MongoDB` documents that stores the size. |
77 | | const SIZE_FIELD: &str = "size"; |
78 | | |
79 | | /// A [`StoreDriver`] implementation that uses `MongoDB` as a backing store. |
80 | | #[derive(Debug, MetricsComponent)] |
81 | | pub struct ExperimentalMongoStore { |
82 | | /// The `MongoDB` client. |
83 | | #[allow(dead_code)] |
84 | | client: MongoClient, |
85 | | |
86 | | /// The database to use. |
87 | | database: Database, |
88 | | |
89 | | /// The collection for CAS data. |
90 | | cas_collection: Collection<Document>, |
91 | | |
92 | | /// The collection for scheduler data. |
93 | | scheduler_collection: Collection<Document>, |
94 | | |
95 | | /// A common prefix to append to all keys before they are sent to `MongoDB`. |
96 | | #[metric(help = "Prefix to append to all keys before sending to MongoDB")] |
97 | | key_prefix: String, |
98 | | |
99 | | /// The amount of data to read from `MongoDB` at a time. |
100 | | #[metric(help = "The amount of data to read from MongoDB at a time")] |
101 | | read_chunk_size: usize, |
102 | | |
103 | | /// Enable change streams for real-time updates. |
104 | | #[metric(help = "Whether change streams are enabled")] |
105 | | enable_change_streams: bool, |
106 | | |
107 | | /// A manager for subscriptions to keys in `MongoDB`. |
108 | | subscription_manager: Mutex<Option<Arc<ExperimentalMongoSubscriptionManager>>>, |
109 | | |
110 | | /// Limits the number of requests at any one time |
111 | | request_permits: Arc<Semaphore>, |
112 | | |
113 | | /// Keep track of the `request_permits` queue size |
114 | | waiting_permits: Arc<AtomicUsize>, |
115 | | } |
116 | | |
117 | | impl ExperimentalMongoStore { |
118 | | /// Create a new `ExperimentalMongoStore` from the given configuration. |
119 | 16 | pub async fn new(mut spec: ExperimentalMongoSpec) -> Result<Arc<Self>, Error> { |
120 | | // Set defaults |
121 | 16 | if spec.connection_string.is_empty() { |
122 | 1 | return Err(make_err!( |
123 | 1 | Code::InvalidArgument, |
124 | 1 | "No connection string was specified in mongo store configuration." |
125 | 1 | )); |
126 | 15 | } |
127 | | |
128 | 15 | if spec.database.is_empty() { |
129 | 1 | spec.database = DEFAULT_DB_NAME.to_string(); |
130 | 14 | } |
131 | | |
132 | 15 | if spec.cas_collection.is_empty() { |
133 | 1 | spec.cas_collection = DEFAULT_COLLECTION_NAME.to_string(); |
134 | 14 | } |
135 | | |
136 | 15 | if spec.scheduler_collection.is_empty() { |
137 | 1 | spec.scheduler_collection = DEFAULT_SCHEDULER_COLLECTION_NAME.to_string(); |
138 | 14 | } |
139 | | |
140 | 15 | if spec.read_chunk_size == 0 { |
141 | 1 | spec.read_chunk_size = DEFAULT_READ_CHUNK_SIZE; |
142 | 14 | } |
143 | | |
144 | 15 | if spec.connection_timeout_ms == 0 { |
145 | 1 | spec.connection_timeout_ms = DEFAULT_CONNECTION_TIMEOUT_MS; |
146 | 14 | } |
147 | | |
148 | 15 | if spec.command_timeout_ms == 0 { |
149 | 1 | spec.command_timeout_ms = DEFAULT_COMMAND_TIMEOUT_MS; |
150 | 14 | } |
151 | | |
152 | 15 | if spec.max_concurrent_uploads != 0 { |
153 | 14 | warn!( |
154 | | "max_concurrent_uploads was set for Mongo, and it's a deprecated value we don't use anymore" |
155 | | ); |
156 | 1 | } |
157 | | |
158 | 15 | if let Some(max_permits2 ) = spec.max_requests |
159 | 2 | && max_permits == 0 |
160 | | { |
161 | 1 | return Err(make_err!( |
162 | 1 | Code::InvalidArgument, |
163 | 1 | "max_request_permits was set to zero, which will block mongo_store from working at all" |
164 | 1 | )); |
165 | 14 | } |
166 | | |
167 | | // Configure client options |
168 | 14 | let mut client_options13 = ClientOptions::parse(&spec.connection_string) |
169 | 14 | .await |
170 | 14 | .map_err(|e| {1 |
171 | 1 | make_err!( |
172 | 1 | Code::InvalidArgument, |
173 | | "Failed to parse MongoDB connection string: {e}" |
174 | | ) |
175 | 1 | })?; |
176 | | |
177 | 13 | client_options.server_selection_timeout = |
178 | 13 | Some(Duration::from_millis(spec.connection_timeout_ms)); |
179 | 13 | client_options.connect_timeout = Some(Duration::from_millis(spec.connection_timeout_ms)); |
180 | | |
181 | | // Set write concern if specified |
182 | 13 | if let Some(w12 ) = spec.write_concern_w { |
183 | | // Parse write concern w - can be a number or string like "majority" |
184 | 12 | let w_value = if let Ok(num0 ) = w.parse::<u32>() { |
185 | 0 | Some(num.into()) |
186 | | } else { |
187 | 12 | Some(w.into()) |
188 | | }; |
189 | | |
190 | | // Build write concern based on which options are set |
191 | 12 | let write_concern = match (w_value, spec.write_concern_j, spec.write_concern_timeout_ms) |
192 | | { |
193 | 12 | (Some(w), Some(j), Some(timeout)) => WriteConcern::builder() |
194 | 12 | .w(Some(w)) |
195 | 12 | .journal(j) |
196 | 12 | .w_timeout(Some(Duration::from_millis(u64::from(timeout)))) |
197 | 12 | .build(), |
198 | 0 | (Some(w), Some(j), None) => WriteConcern::builder().w(Some(w)).journal(j).build(), |
199 | 0 | (Some(w), None, Some(timeout)) => WriteConcern::builder() |
200 | 0 | .w(Some(w)) |
201 | 0 | .w_timeout(Some(Duration::from_millis(u64::from(timeout)))) |
202 | 0 | .build(), |
203 | 0 | (Some(w), None, None) => WriteConcern::builder().w(Some(w)).build(), |
204 | 0 | _ => unreachable!(), // We know w is Some because we're in the if let Some(w) block |
205 | | }; |
206 | | |
207 | 12 | client_options.write_concern = Some(write_concern); |
208 | 1 | } else if spec.write_concern_j.is_some() || spec.write_concern_timeout_ms0 .is_some0 () { |
209 | 1 | return Err(make_err!( |
210 | 1 | Code::InvalidArgument, |
211 | 1 | "write_concern_w not set, but j and/or timeout set. Please set 'write_concern_w' to a non-default value. See https://www.mongodb.com/docs/manual/reference/write-concern/#w-option for options." |
212 | 1 | )); |
213 | 0 | } |
214 | | |
215 | | // Create client |
216 | 12 | let client = MongoClient::with_options(client_options).map_err(|e| {0 |
217 | 0 | make_err!( |
218 | 0 | Code::InvalidArgument, |
219 | | "Failed to create MongoDB client: {e}" |
220 | | ) |
221 | 0 | })?; |
222 | | |
223 | | // Get database and collections |
224 | 12 | let database = client.database(&spec.database); |
225 | 12 | let cas_collection = database.collection::<Document>(&spec.cas_collection); |
226 | 12 | let scheduler_collection = database.collection::<Document>(&spec.scheduler_collection); |
227 | | |
228 | | // Create indexes |
229 | 12 | Self::create_indexes(&cas_collection, &scheduler_collection).await?0 ; |
230 | | |
231 | 12 | let store = Self { |
232 | 12 | client, |
233 | 12 | database, |
234 | 12 | cas_collection, |
235 | 12 | scheduler_collection, |
236 | 12 | key_prefix: spec.key_prefix.clone().unwrap_or_default(), |
237 | 12 | read_chunk_size: spec.read_chunk_size, |
238 | 12 | enable_change_streams: spec.enable_change_streams, |
239 | 12 | subscription_manager: Mutex::new(None), |
240 | 12 | request_permits: Arc::new(Semaphore::new( |
241 | 12 | spec.max_requests.unwrap_or(Semaphore::MAX_PERMITS), |
242 | 12 | )), |
243 | 12 | waiting_permits: Arc::new(AtomicUsize::new(0)), |
244 | 12 | }; |
245 | | |
246 | 12 | Ok(Arc::new(store)) |
247 | 16 | } |
248 | | |
249 | | /// Create necessary indexes for efficient operations. |
250 | 12 | async fn create_indexes( |
251 | 12 | cas_collection: &Collection<Document>, |
252 | 12 | _scheduler_collection: &Collection<Document>, |
253 | 12 | ) -> Result<(), Error> { |
254 | | // CAS collection indexes |
255 | 12 | cas_collection |
256 | 12 | .create_index(IndexModel::builder().keys(doc! { SIZE_FIELD: 1 }).build()) |
257 | 12 | .await |
258 | 12 | .map_err(|e| {0 |
259 | 0 | make_err!( |
260 | 0 | Code::Internal, |
261 | | "Failed to create size index on CAS collection: {e}" |
262 | | ) |
263 | 0 | })?; |
264 | | |
265 | | // Scheduler collection will have dynamic indexes created as needed |
266 | | |
267 | 12 | Ok(()) |
268 | 12 | } |
269 | | |
270 | | /// Encode a [`StoreKey`] so it can be sent to `MongoDB`. |
271 | 55 | fn encode_key<'a>(&self, key: &'a StoreKey<'a>) -> Cow<'a, str> { |
272 | 55 | let key_body = key.as_str(); |
273 | 55 | if self.key_prefix.is_empty() { |
274 | 55 | key_body |
275 | | } else { |
276 | 0 | match key_body { |
277 | 0 | Cow::Owned(mut encoded_key) => { |
278 | 0 | encoded_key.insert_str(0, &self.key_prefix); |
279 | 0 | Cow::Owned(encoded_key) |
280 | | } |
281 | 0 | Cow::Borrowed(body) => { |
282 | 0 | let mut encoded_key = String::with_capacity(self.key_prefix.len() + body.len()); |
283 | 0 | encoded_key.push_str(&self.key_prefix); |
284 | 0 | encoded_key.push_str(body); |
285 | 0 | Cow::Owned(encoded_key) |
286 | | } |
287 | | } |
288 | | } |
289 | 55 | } |
290 | | |
291 | | /// Decode a key from `MongoDB` by removing the prefix. |
292 | 0 | fn decode_key(&self, key: &str) -> Option<String> { |
293 | 0 | if self.key_prefix.is_empty() { |
294 | 0 | Some(key.to_string()) |
295 | | } else { |
296 | 0 | key.strip_prefix(&self.key_prefix).map(ToString::to_string) |
297 | | } |
298 | 0 | } |
299 | | |
300 | 53 | async fn acquire_permit(&self) -> Result<SemaphorePermit<'_>, Error> { |
301 | 53 | let waiting = self.waiting_permits.fetch_add(1, Ordering::Relaxed); |
302 | | |
303 | 53 | if waiting > 0 && waiting0 .is_multiple_of0 (100) { |
304 | 0 | info!(waiting, "Number of waiting permits for Mongo"); |
305 | | } else { |
306 | 53 | trace!(waiting, "Number of waiting permits for Mongo"); |
307 | | } |
308 | 53 | let permit = self.request_permits.acquire().await; |
309 | 53 | self.waiting_permits.fetch_sub(1, Ordering::Relaxed); |
310 | 53 | Ok(permit?0 ) |
311 | 53 | } |
312 | | } |
313 | | |
314 | | #[async_trait] |
315 | | impl StoreDriver for ExperimentalMongoStore { |
316 | 0 | async fn post_init(self: Arc<Self>) -> Result<(), Error> { |
317 | | Ok(()) |
318 | 0 | } |
319 | | |
320 | | async fn has_with_results( |
321 | | self: Pin<&Self>, |
322 | | keys: &[StoreKey<'_>], |
323 | | results: &mut [Option<u64>], |
324 | 18 | ) -> Result<(), Error> { |
325 | | for (key, result) in keys.iter().zip(results.iter_mut()) { |
326 | | // Handle zero digest specially |
327 | | if is_zero_digest(key.borrow()) { |
328 | | *result = Some(0); |
329 | | continue; |
330 | | } |
331 | | |
332 | | let encoded_key = self.encode_key(key); |
333 | | let filter = doc! { KEY_FIELD: encoded_key.as_ref() }; |
334 | | |
335 | | // We could do this with acquire_many, but that's unsafe if the number of keys is greater |
336 | | // than the number of permits, as it'll block forever. Doing this one at a time is guaranteed |
337 | | // not to block provided no-one sets permits to 0, and we check for that case at startup. |
338 | | let semaphore = self.acquire_permit().await?; |
339 | | |
340 | | match self.cas_collection.find_one(filter).await { |
341 | | Ok(Some(doc)) => { |
342 | 17 | *result = doc.get_i64(SIZE_FIELD).ok().map(|v| v as u64); |
343 | | } |
344 | | Ok(None) => { |
345 | | *result = None; |
346 | | } |
347 | | Err(e) => { |
348 | | return Err(make_err!( |
349 | | Code::Internal, |
350 | | "MongoDB error in has_with_results: {e}" |
351 | | )); |
352 | | } |
353 | | } |
354 | | drop(semaphore); |
355 | | } |
356 | | |
357 | | Ok(()) |
358 | 18 | } |
359 | | |
360 | | async fn list( |
361 | | self: Pin<&Self>, |
362 | | range: (Bound<StoreKey<'_>>, Bound<StoreKey<'_>>), |
363 | | handler: &mut (dyn for<'a> FnMut(&'a StoreKey) -> bool + Send + Sync + '_), |
364 | 0 | ) -> Result<u64, Error> { |
365 | | let mut filter = Document::new(); |
366 | | |
367 | | // Build range query |
368 | | let mut key_filter = Document::new(); |
369 | | match &range.0 { |
370 | | Bound::Included(start) => { |
371 | | let encoded = self.encode_key(start); |
372 | | key_filter.insert("$gte", encoded.as_ref()); |
373 | | } |
374 | | Bound::Excluded(start) => { |
375 | | let encoded = self.encode_key(start); |
376 | | key_filter.insert("$gt", encoded.as_ref()); |
377 | | } |
378 | | Bound::Unbounded => {} |
379 | | } |
380 | | |
381 | | match &range.1 { |
382 | | Bound::Included(end) => { |
383 | | let encoded = self.encode_key(end); |
384 | | key_filter.insert("$lte", encoded.as_ref()); |
385 | | } |
386 | | Bound::Excluded(end) => { |
387 | | let encoded = self.encode_key(end); |
388 | | key_filter.insert("$lt", encoded.as_ref()); |
389 | | } |
390 | | Bound::Unbounded => {} |
391 | | } |
392 | | |
393 | | if !key_filter.is_empty() { |
394 | | filter.insert(KEY_FIELD, key_filter); |
395 | | } |
396 | | |
397 | | // Add prefix filter if needed |
398 | | if !self.key_prefix.is_empty() { |
399 | | let regex_filter = doc! { |
400 | | KEY_FIELD: { |
401 | | "$regex": format!("^{}", regex::escape(&self.key_prefix)), |
402 | | } |
403 | | }; |
404 | | if filter.is_empty() { |
405 | | filter = regex_filter; |
406 | | } else { |
407 | | filter = doc! { "$and": [filter, regex_filter] }; |
408 | | } |
409 | | } |
410 | | |
411 | | let semaphore = self.acquire_permit().await?; |
412 | | |
413 | | let mut cursor = self |
414 | | .cas_collection |
415 | | .find(filter) |
416 | | .projection(doc! { KEY_FIELD: 1 }) |
417 | | .await |
418 | 0 | .map_err(|e| make_err!(Code::Internal, "Failed to create cursor in list: {e}"))?; |
419 | | |
420 | | let mut count = 0u64; |
421 | | while let Some(doc) = cursor |
422 | | .try_next() |
423 | | .await |
424 | 0 | .map_err(|e| make_err!(Code::Internal, "Failed to get next document in list: {e}"))? |
425 | | { |
426 | | if let Ok(key) = doc.get_str(KEY_FIELD) |
427 | | && let Some(decoded_key) = self.decode_key(key) |
428 | | { |
429 | | let store_key = StoreKey::new_str(&decoded_key); |
430 | | count += 1; |
431 | | if !handler(&store_key) { |
432 | | break; |
433 | | } |
434 | | } |
435 | | } |
436 | | |
437 | | drop(semaphore); |
438 | | |
439 | | Ok(count) |
440 | 0 | } |
441 | | |
442 | | async fn update( |
443 | | self: Pin<&Self>, |
444 | | key: StoreKey<'_>, |
445 | | mut reader: DropCloserReadHalf, |
446 | | upload_size: UploadSizeInfo, |
447 | 20 | ) -> Result<u64, Error> { |
448 | | let encoded_key = self.encode_key(&key); |
449 | | |
450 | | // Handle zero digest |
451 | | if is_zero_digest(key.borrow()) { |
452 | 0 | let chunk = reader.peek().await.map_err(|e| { |
453 | 0 | make_err!( |
454 | 0 | Code::Internal, |
455 | | "Failed to peek in ExperimentalMongoStore::update: {e}" |
456 | | ) |
457 | 0 | })?; |
458 | | if chunk.is_empty() { |
459 | 0 | reader.drain().await.map_err(|e| { |
460 | 0 | make_err!( |
461 | 0 | Code::Internal, |
462 | | "Failed to drain in ExperimentalMongoStore::update: {e}" |
463 | | ) |
464 | 0 | })?; |
465 | | return Ok(0); |
466 | | } |
467 | | } |
468 | | |
469 | | // Special handling for MaxSize(0) - this should error if stream is closed |
470 | | if upload_size == UploadSizeInfo::MaxSize(0) { |
471 | | // Try to read from the stream - if it's closed immediately, this should error |
472 | | match reader.recv().await { |
473 | | Ok(_chunk) => { |
474 | | return Err(make_input_err!( |
475 | | "Received data when MaxSize is 0 in MongoDB store" |
476 | | )); |
477 | | } |
478 | | Err(e) => { |
479 | | return Err(make_err!( |
480 | | Code::InvalidArgument, |
481 | | "Stream closed for MaxSize(0) upload: {e}" |
482 | | )); |
483 | | } |
484 | | } |
485 | | } |
486 | | |
487 | | // Read all data into memory with proper EOF handling |
488 | | let mut data = Vec::new(); |
489 | | while let Ok(chunk) = reader.recv().await { |
490 | | if chunk.is_empty() { |
491 | | break; // Empty chunk signals EOF |
492 | | } |
493 | | data.extend_from_slice(&chunk); |
494 | | } |
495 | | |
496 | | let size = data.len() as i64; |
497 | | |
498 | | // Create document |
499 | | let doc = doc! { |
500 | | KEY_FIELD: encoded_key.as_ref(), |
501 | | DATA_FIELD: Bson::Binary(mongodb::bson::Binary { |
502 | | subtype: mongodb::bson::spec::BinarySubtype::Generic, |
503 | | bytes: data, |
504 | | }), |
505 | | SIZE_FIELD: size, |
506 | | }; |
507 | | |
508 | | let semaphore = self.acquire_permit().await?; |
509 | | |
510 | | // Upsert the document |
511 | | self.cas_collection |
512 | | .update_one( |
513 | | doc! { KEY_FIELD: encoded_key.as_ref() }, |
514 | | doc! { "$set": doc }, |
515 | | ) |
516 | | .upsert(true) |
517 | | .await |
518 | 0 | .map_err(|e| make_err!(Code::Internal, "Failed to update document in MongoDB: {e}"))?; |
519 | | |
520 | | drop(semaphore); |
521 | | |
522 | | Ok(size.try_into().unwrap_or(0)) |
523 | 20 | } |
524 | | |
525 | | async fn get_part( |
526 | | self: Pin<&Self>, |
527 | | key: StoreKey<'_>, |
528 | | writer: &mut DropCloserWriteHalf, |
529 | | offset: u64, |
530 | | length: Option<u64>, |
531 | 18 | ) -> Result<(), Error> { |
532 | | // Handle zero digest |
533 | | if is_zero_digest(key.borrow()) { |
534 | 0 | return writer.send_eof().map_err(|e| { |
535 | 0 | make_err!( |
536 | 0 | Code::Internal, |
537 | | "Failed to send zero EOF in mongo store get_part: {e}" |
538 | | ) |
539 | 0 | }); |
540 | | } |
541 | | |
542 | | let encoded_key = self.encode_key(&key); |
543 | | let filter = doc! { KEY_FIELD: encoded_key.as_ref() }; |
544 | | |
545 | | let semaphore = self.acquire_permit().await?; |
546 | | |
547 | | let doc = self |
548 | | .cas_collection |
549 | | .find_one(filter) |
550 | | .await |
551 | 0 | .map_err(|e| make_err!(Code::Internal, "Failed to find document in get_part: {e}"))? |
552 | 1 | .ok_or_else(|| { |
553 | 1 | make_err!( |
554 | 1 | Code::NotFound, |
555 | | "Data not found in MongoDB store for digest: {key:?}" |
556 | | ) |
557 | 1 | })?; |
558 | | |
559 | | let data = match doc.get(DATA_FIELD) { |
560 | | Some(Bson::Binary(binary)) => &binary.bytes, |
561 | | _ => { |
562 | | return Err(make_err!( |
563 | | Code::Internal, |
564 | | "Invalid data field in MongoDB document" |
565 | | )); |
566 | | } |
567 | | }; |
568 | | |
569 | | let offset = usize::try_from(offset).unwrap_or(usize::MAX); |
570 | | let data_len = data.len(); |
571 | | |
572 | | if offset > data_len { |
573 | 0 | return writer.send_eof().map_err(|e| { |
574 | 0 | make_err!( |
575 | 0 | Code::Internal, |
576 | | "Failed to send EOF in mongo store get_part: {e}" |
577 | | ) |
578 | 0 | }); |
579 | | } |
580 | | |
581 | | let end = if let Some(len) = length { |
582 | | cmp::min( |
583 | | offset.saturating_add(usize::try_from(len).unwrap_or(usize::MAX)), |
584 | | data_len, |
585 | | ) |
586 | | } else { |
587 | | data_len |
588 | | }; |
589 | | |
590 | | if offset < end { |
591 | | let chunk = &data[offset..end]; |
592 | | |
593 | | // Send data in chunks |
594 | | for chunk_data in chunk.chunks(self.read_chunk_size) { |
595 | 0 | writer.send(chunk_data.to_vec().into()).await.map_err(|e| { |
596 | 0 | make_err!( |
597 | 0 | Code::Internal, |
598 | | "Failed to write data in ExperimentalMongoStore::get_part: {e}" |
599 | | ) |
600 | 0 | })?; |
601 | | } |
602 | | } |
603 | | |
604 | | drop(semaphore); |
605 | | |
606 | 0 | writer.send_eof().map_err(|e| { |
607 | 0 | make_err!( |
608 | 0 | Code::Internal, |
609 | | "Failed to write EOF in mongo store get_part: {e}" |
610 | | ) |
611 | 0 | }) |
612 | 18 | } |
613 | | |
614 | 0 | fn inner_store(&self, _digest: Option<StoreKey>) -> &dyn StoreDriver { |
615 | 0 | self |
616 | 0 | } |
617 | | |
618 | 0 | fn as_any(&self) -> &(dyn core::any::Any + Sync + Send) { |
619 | 0 | self |
620 | 0 | } |
621 | | |
622 | 0 | fn as_any_arc(self: Arc<Self>) -> Arc<dyn core::any::Any + Sync + Send> { |
623 | 0 | self |
624 | 0 | } |
625 | | |
626 | 0 | fn register_health(self: Arc<Self>, registry: &mut HealthRegistryBuilder) { |
627 | 0 | registry.register_indicator(self); |
628 | 0 | } |
629 | | |
630 | 0 | fn register_remove_callback(self: Arc<Self>, _callback: RemoveCallback) -> Result<(), Error> { |
631 | | // drop because we don't remove anything from Mongo |
632 | 0 | Ok(()) |
633 | 0 | } |
634 | | } |
635 | | |
636 | | #[async_trait] |
637 | | impl HealthStatusIndicator for ExperimentalMongoStore { |
638 | 0 | fn get_name(&self) -> &'static str { |
639 | 0 | "ExperimentalMongoStore" |
640 | 0 | } |
641 | | |
642 | 0 | async fn check_health(&self, namespace: Cow<'static, str>) -> HealthStatus { |
643 | | // Note we do not acquire a request_permit here, as the health check needs to always go through |
644 | | // even if everything else is fully loaded |
645 | | match self.database.run_command(doc! { "ping": 1 }).await { |
646 | | Ok(_) => HealthStatus::new_ok(self, "Connection healthy".into()), |
647 | | Err(e) => HealthStatus::new_failed( |
648 | | self, |
649 | | format!("{namespace} - MongoDB connection error: {e}").into(), |
650 | | ), |
651 | | } |
652 | 0 | } |
653 | | } |
654 | | |
655 | | // ------------------------------------------------------------------- |
656 | | // |
657 | | // `ExperimentalMongoDB` scheduler implementation. Likely to change |
658 | | // ------------------------------------------------------------------- |
659 | | |
660 | | /// An individual subscription to a key in `ExperimentalMongoDB`. |
661 | | #[derive(Debug)] |
662 | | pub struct ExperimentalMongoSubscription { |
663 | | receiver: Option<watch::Receiver<String>>, |
664 | | weak_subscribed_keys: Weak<RwLock<StringPatriciaMap<ExperimentalMongoSubscriptionPublisher>>>, |
665 | | } |
666 | | |
667 | | impl SchedulerSubscription for ExperimentalMongoSubscription { |
668 | 0 | async fn changed(&mut self) -> Result<(), Error> { |
669 | 0 | let receiver = self.receiver.as_mut().ok_or_else(|| { |
670 | 0 | make_err!( |
671 | 0 | Code::Internal, |
672 | | "In ExperimentalMongoSubscription::changed::as_mut" |
673 | | ) |
674 | 0 | })?; |
675 | 0 | receiver.changed().await.map_err(|err| { |
676 | 0 | Error::from_std_err(Code::Internal, &err) |
677 | 0 | .append("In ExperimentalMongoSubscription::changed::changed") |
678 | 0 | }) |
679 | 0 | } |
680 | | } |
681 | | |
682 | | impl Drop for ExperimentalMongoSubscription { |
683 | 0 | fn drop(&mut self) { |
684 | 0 | let Some(receiver) = self.receiver.take() else { |
685 | 0 | warn!("ExperimentalMongoSubscription has already been dropped, nothing to do."); |
686 | 0 | return; |
687 | | }; |
688 | 0 | let key = receiver.borrow().clone(); |
689 | 0 | drop(receiver); |
690 | | |
691 | 0 | let Some(subscribed_keys) = self.weak_subscribed_keys.upgrade() else { |
692 | 0 | return; |
693 | | }; |
694 | 0 | let mut subscribed_keys = subscribed_keys.write(); |
695 | 0 | let Some(value) = subscribed_keys.get(&key) else { |
696 | 0 | error!( |
697 | | "Key {key} was not found in subscribed keys when checking if it should be removed." |
698 | | ); |
699 | 0 | return; |
700 | | }; |
701 | | |
702 | 0 | if value.receiver_count() == 0 { |
703 | 0 | subscribed_keys.remove(key); |
704 | 0 | } |
705 | 0 | } |
706 | | } |
707 | | |
708 | | /// A publisher for a key in `ExperimentalMongoDB`. |
709 | | #[derive(Debug)] |
710 | | struct ExperimentalMongoSubscriptionPublisher { |
711 | | sender: Mutex<watch::Sender<String>>, |
712 | | } |
713 | | |
714 | | impl ExperimentalMongoSubscriptionPublisher { |
715 | 0 | fn new( |
716 | 0 | key: String, |
717 | 0 | weak_subscribed_keys: Weak<RwLock<StringPatriciaMap<Self>>>, |
718 | 0 | ) -> (Self, ExperimentalMongoSubscription) { |
719 | 0 | let (sender, receiver) = watch::channel(key); |
720 | 0 | let publisher = Self { |
721 | 0 | sender: Mutex::new(sender), |
722 | 0 | }; |
723 | 0 | let subscription = ExperimentalMongoSubscription { |
724 | 0 | receiver: Some(receiver), |
725 | 0 | weak_subscribed_keys, |
726 | 0 | }; |
727 | 0 | (publisher, subscription) |
728 | 0 | } |
729 | | |
730 | 0 | fn subscribe( |
731 | 0 | &self, |
732 | 0 | weak_subscribed_keys: Weak<RwLock<StringPatriciaMap<Self>>>, |
733 | 0 | ) -> ExperimentalMongoSubscription { |
734 | 0 | let receiver = self.sender.lock().subscribe(); |
735 | 0 | ExperimentalMongoSubscription { |
736 | 0 | receiver: Some(receiver), |
737 | 0 | weak_subscribed_keys, |
738 | 0 | } |
739 | 0 | } |
740 | | |
741 | 0 | fn receiver_count(&self) -> usize { |
742 | 0 | self.sender.lock().receiver_count() |
743 | 0 | } |
744 | | |
745 | 0 | fn notify(&self) { |
746 | 0 | self.sender.lock().send_modify(|_| {}); |
747 | 0 | } |
748 | | } |
749 | | |
750 | | #[derive(Debug)] |
751 | | pub struct ExperimentalMongoSubscriptionManager { |
752 | | subscribed_keys: Arc<RwLock<StringPatriciaMap<ExperimentalMongoSubscriptionPublisher>>>, |
753 | | _subscription_spawn: JoinHandleDropGuard<()>, |
754 | | } |
755 | | |
756 | | impl ExperimentalMongoSubscriptionManager { |
757 | 0 | pub fn new(database: Database, collection_name: String, key_prefix: String) -> Self { |
758 | 0 | let subscribed_keys = Arc::new(RwLock::new(StringPatriciaMap::new())); |
759 | 0 | let subscribed_keys_weak = Arc::downgrade(&subscribed_keys); |
760 | | |
761 | | Self { |
762 | 0 | subscribed_keys, |
763 | 0 | _subscription_spawn: spawn!("mongo_subscribe_spawn", async move { |
764 | 0 | let collection = database.collection::<Document>(&collection_name); |
765 | | |
766 | | loop { |
767 | | // Try to create change stream |
768 | 0 | let change_stream_result = collection.watch().await; |
769 | | |
770 | 0 | match change_stream_result { |
771 | 0 | Ok(mut change_stream) => { |
772 | 0 | info!("MongoDB change stream connected"); |
773 | | |
774 | 0 | while let Some(event_result) = change_stream.next().await { |
775 | 0 | match event_result { |
776 | 0 | Ok(event) => { |
777 | | use mongodb::change_stream::event::OperationType; |
778 | 0 | match event.operation_type { |
779 | | OperationType::Insert |
780 | | | OperationType::Update |
781 | | | OperationType::Replace |
782 | | | OperationType::Delete => { |
783 | 0 | if let Some(doc_key) = event.document_key |
784 | 0 | && let Ok(key) = doc_key.get_str(KEY_FIELD) |
785 | | { |
786 | | // Remove prefix if present |
787 | 0 | let key = if key_prefix.is_empty() { |
788 | 0 | key |
789 | | } else { |
790 | 0 | key.strip_prefix(&key_prefix).unwrap_or(key) |
791 | | }; |
792 | | |
793 | 0 | let Some(subscribed_keys) = |
794 | 0 | subscribed_keys_weak.upgrade() |
795 | | else { |
796 | 0 | warn!( |
797 | | "Parent dropped, exiting ExperimentalMongoSubscriptionManager" |
798 | | ); |
799 | 0 | return; |
800 | | }; |
801 | | |
802 | 0 | let subscribed_keys_mux = |
803 | 0 | subscribed_keys.read(); |
804 | 0 | subscribed_keys_mux |
805 | 0 | .common_prefix_values(key) |
806 | 0 | .for_each(ExperimentalMongoSubscriptionPublisher::notify); |
807 | 0 | } |
808 | | } |
809 | 0 | _ => {} |
810 | | } |
811 | | } |
812 | 0 | Err(e) => { |
813 | 0 | error!("Error in change stream: {e}"); |
814 | 0 | break; |
815 | | } |
816 | | } |
817 | | } |
818 | | } |
819 | 0 | Err(e) => { |
820 | 0 | warn!("Failed to create change stream: {e}. Will retry in 5 seconds."); |
821 | | } |
822 | | } |
823 | | |
824 | | // Check if parent is still alive |
825 | 0 | if subscribed_keys_weak.upgrade().is_none() { |
826 | 0 | warn!("Parent dropped, exiting ExperimentalMongoSubscriptionManager"); |
827 | 0 | return; |
828 | 0 | } |
829 | | |
830 | | // Sleep before retry |
831 | 0 | sleep(Duration::from_secs(5)).await; |
832 | | |
833 | | // Notify all subscribers on reconnection |
834 | 0 | if let Some(subscribed_keys) = subscribed_keys_weak.upgrade() { |
835 | 0 | let subscribed_keys_mux = subscribed_keys.read(); |
836 | 0 | for publisher in subscribed_keys_mux.values() { |
837 | 0 | publisher.notify(); |
838 | 0 | } |
839 | 0 | } |
840 | | } |
841 | 0 | }), |
842 | | } |
843 | 0 | } |
844 | | } |
845 | | |
846 | | impl SchedulerSubscriptionManager for ExperimentalMongoSubscriptionManager { |
847 | | type Subscription = ExperimentalMongoSubscription; |
848 | | |
849 | 0 | fn subscribe<K>(&self, key: K) -> Result<Self::Subscription, Error> |
850 | 0 | where |
851 | 0 | K: SchedulerStoreKeyProvider, |
852 | | { |
853 | 0 | let weak_subscribed_keys = Arc::downgrade(&self.subscribed_keys); |
854 | 0 | let mut subscribed_keys = self.subscribed_keys.write(); |
855 | 0 | let key = key.get_key(); |
856 | 0 | let key_str = key.as_str(); |
857 | | |
858 | 0 | let mut subscription = if let Some(publisher) = subscribed_keys.get(&key_str) { |
859 | 0 | publisher.subscribe(weak_subscribed_keys) |
860 | | } else { |
861 | 0 | let (publisher, subscription) = ExperimentalMongoSubscriptionPublisher::new( |
862 | 0 | key_str.to_string(), |
863 | 0 | weak_subscribed_keys, |
864 | 0 | ); |
865 | 0 | subscribed_keys.insert(key_str, publisher); |
866 | 0 | subscription |
867 | | }; |
868 | | |
869 | 0 | subscription |
870 | 0 | .receiver |
871 | 0 | .as_mut() |
872 | 0 | .ok_or_else(|| { |
873 | 0 | make_err!( |
874 | 0 | Code::Internal, |
875 | | "Receiver should be set in ExperimentalMongoSubscriptionManager::subscribe" |
876 | | ) |
877 | 0 | })? |
878 | 0 | .mark_changed(); |
879 | | |
880 | 0 | Ok(subscription) |
881 | 0 | } |
882 | | |
883 | 0 | fn is_reliable() -> bool { |
884 | 0 | true |
885 | 0 | } |
886 | | } |
887 | | |
888 | | impl SchedulerStore for ExperimentalMongoStore { |
889 | | type SubscriptionManager = ExperimentalMongoSubscriptionManager; |
890 | | |
891 | 0 | async fn subscription_manager( |
892 | 0 | &self, |
893 | 0 | ) -> Result<Arc<ExperimentalMongoSubscriptionManager>, Error> { |
894 | 0 | let mut subscription_manager = self.subscription_manager.lock(); |
895 | 0 | if let Some(subscription_manager) = &*subscription_manager { |
896 | 0 | Ok(subscription_manager.clone()) |
897 | | } else { |
898 | 0 | if !self.enable_change_streams { |
899 | 0 | return Err(make_input_err!( |
900 | 0 | "ExperimentalMongoStore must have change streams enabled for scheduler subscriptions" |
901 | 0 | )); |
902 | 0 | } |
903 | | |
904 | 0 | let sub = Arc::new(ExperimentalMongoSubscriptionManager::new( |
905 | 0 | self.database.clone(), |
906 | 0 | self.scheduler_collection.name().to_string(), |
907 | 0 | self.key_prefix.clone(), |
908 | | )); |
909 | 0 | *subscription_manager = Some(sub.clone()); |
910 | 0 | Ok(sub) |
911 | | } |
912 | 0 | } |
913 | | |
914 | 0 | async fn update_data<T>(&self, data: T, expiry: Option<Duration>) -> Result<Option<i64>, Error> |
915 | 0 | where |
916 | 0 | T: SchedulerStoreDataProvider |
917 | 0 | + SchedulerStoreKeyProvider |
918 | 0 | + SchedulerCurrentVersionProvider |
919 | 0 | + Send, |
920 | 0 | { |
921 | 0 | if expiry.is_some() { |
922 | 0 | return Err(make_err!( |
923 | 0 | Code::InvalidArgument, |
924 | 0 | "Mongo store doesn't support expiry!" |
925 | 0 | )); |
926 | 0 | } |
927 | 0 | let key = data.get_key(); |
928 | 0 | let encoded_key = self.encode_key(&key); |
929 | 0 | let maybe_index = data.get_indexes().map_err(|e| { |
930 | 0 | make_err!( |
931 | 0 | Code::Internal, |
932 | | "Error getting indexes in ExperimentalMongoStore::update_data: {e}" |
933 | | ) |
934 | 0 | })?; |
935 | | |
936 | 0 | if <T as SchedulerStoreKeyProvider>::Versioned::VALUE { |
937 | 0 | let current_version = data.current_version(); |
938 | 0 | let data_bytes = data.try_into_bytes().map_err(|e| { |
939 | 0 | make_err!( |
940 | 0 | Code::Internal, |
941 | | "Could not convert value to bytes in ExperimentalMongoStore::update_data: {e}" |
942 | | ) |
943 | 0 | })?; |
944 | | |
945 | 0 | let mut update_doc = doc! { |
946 | | "$set": { |
947 | 0 | DATA_FIELD: Bson::Binary(mongodb::bson::Binary { |
948 | 0 | subtype: mongodb::bson::spec::BinarySubtype::Generic, |
949 | 0 | bytes: data_bytes.to_vec(), |
950 | 0 | }), |
951 | | }, |
952 | | "$inc": { |
953 | | VERSION_FIELD: 1i64, |
954 | | } |
955 | | }; |
956 | | |
957 | | // Add indexes |
958 | 0 | for (name, value) in maybe_index { |
959 | 0 | update_doc.get_document_mut("$set").unwrap().insert( |
960 | 0 | name, |
961 | 0 | Bson::Binary(mongodb::bson::Binary { |
962 | 0 | subtype: mongodb::bson::spec::BinarySubtype::Generic, |
963 | 0 | bytes: value.to_vec(), |
964 | 0 | }), |
965 | 0 | ); |
966 | 0 | } |
967 | | |
968 | 0 | let filter = doc! { |
969 | 0 | KEY_FIELD: encoded_key.as_ref(), |
970 | 0 | VERSION_FIELD: current_version, |
971 | | }; |
972 | | |
973 | 0 | let semaphore = self.acquire_permit().await?; |
974 | | |
975 | 0 | let result = match self |
976 | 0 | .scheduler_collection |
977 | 0 | .find_one_and_update(filter, update_doc) |
978 | 0 | .upsert(true) |
979 | 0 | .return_document(ReturnDocument::After) |
980 | 0 | .await |
981 | | { |
982 | 0 | Ok(Some(doc)) => Ok(doc.get_i64(VERSION_FIELD).ok().or(Some(1i64))), |
983 | 0 | Ok(None) => Ok(None), |
984 | 0 | Err(e) => Err(make_err!( |
985 | 0 | Code::Internal, |
986 | 0 | "MongoDB error in update_data: {e}" |
987 | 0 | )), |
988 | | }; |
989 | 0 | drop(semaphore); |
990 | 0 | result |
991 | | } else { |
992 | 0 | let data_bytes = data.try_into_bytes().map_err(|e| { |
993 | 0 | make_err!( |
994 | 0 | Code::Internal, |
995 | | "Could not convert value to bytes in ExperimentalMongoStore::update_data: {e}" |
996 | | ) |
997 | 0 | })?; |
998 | | |
999 | 0 | let mut doc = doc! { |
1000 | 0 | KEY_FIELD: encoded_key.as_ref(), |
1001 | 0 | DATA_FIELD: Bson::Binary(mongodb::bson::Binary { |
1002 | 0 | subtype: mongodb::bson::spec::BinarySubtype::Generic, |
1003 | 0 | bytes: data_bytes.to_vec(), |
1004 | 0 | }), |
1005 | | }; |
1006 | | |
1007 | | // Add indexes |
1008 | 0 | for (name, value) in maybe_index { |
1009 | 0 | doc.insert( |
1010 | 0 | name, |
1011 | 0 | Bson::Binary(mongodb::bson::Binary { |
1012 | 0 | subtype: mongodb::bson::spec::BinarySubtype::Generic, |
1013 | 0 | bytes: value.to_vec(), |
1014 | 0 | }), |
1015 | 0 | ); |
1016 | 0 | } |
1017 | | |
1018 | 0 | let semaphore = self.acquire_permit().await?; |
1019 | | |
1020 | 0 | self.scheduler_collection |
1021 | 0 | .update_one( |
1022 | 0 | doc! { KEY_FIELD: encoded_key.as_ref() }, |
1023 | 0 | doc! { "$set": doc }, |
1024 | 0 | ) |
1025 | 0 | .upsert(true) |
1026 | 0 | .await |
1027 | 0 | .map_err(|e| { |
1028 | 0 | make_err!(Code::Internal, "Failed to update scheduler document: {e}") |
1029 | 0 | })?; |
1030 | | |
1031 | 0 | drop(semaphore); |
1032 | | |
1033 | 0 | Ok(Some(0)) |
1034 | | } |
1035 | 0 | } |
1036 | | |
1037 | 0 | async fn search_by_index_prefix<K>( |
1038 | 0 | &self, |
1039 | 0 | index: K, |
1040 | 0 | ) -> Result< |
1041 | 0 | impl Stream<Item = Result<<K as SchedulerStoreDecodeTo>::DecodeOutput, Error>> + Send, |
1042 | 0 | Error, |
1043 | 0 | > |
1044 | 0 | where |
1045 | 0 | K: SchedulerIndexProvider + SchedulerStoreDecodeTo + Send, |
1046 | 0 | { |
1047 | 0 | let index_value = index.index_value(); |
1048 | | |
1049 | | // Create index if it doesn't exist |
1050 | 0 | let index_name = format!("{}_{}", K::KEY_PREFIX, K::INDEX_NAME); |
1051 | 0 | self.scheduler_collection |
1052 | 0 | .create_index( |
1053 | 0 | IndexModel::builder() |
1054 | 0 | .keys(doc! { K::INDEX_NAME: 1 }) |
1055 | 0 | .options(IndexOptions::builder().name(index_name).build()) |
1056 | 0 | .build(), |
1057 | 0 | ) |
1058 | 0 | .await |
1059 | 0 | .map_err(|e| make_err!(Code::Internal, "Failed to create scheduler index: {e}"))?; |
1060 | | |
1061 | | // Build filter |
1062 | 0 | let filter = doc! { |
1063 | | K::INDEX_NAME: { |
1064 | 0 | "$regex": format!("^{}", regex::escape(index_value.as_ref())), |
1065 | | } |
1066 | | }; |
1067 | | |
1068 | | // Add sort if specified |
1069 | 0 | let find_options = if let Some(sort_key) = K::MAYBE_SORT_KEY { |
1070 | 0 | FindOptions::builder().sort(doc! { sort_key: 1 }).build() |
1071 | | } else { |
1072 | 0 | FindOptions::default() |
1073 | | }; |
1074 | | |
1075 | 0 | let cursor = self |
1076 | 0 | .scheduler_collection |
1077 | 0 | .find(filter) |
1078 | 0 | .with_options(find_options) |
1079 | 0 | .await |
1080 | 0 | .map_err(|e| { |
1081 | 0 | make_err!( |
1082 | 0 | Code::Internal, |
1083 | | "Failed to create cursor in search_by_index_prefix: {e}" |
1084 | | ) |
1085 | 0 | })?; |
1086 | | |
1087 | 0 | Ok(cursor.map(move |result| { |
1088 | 0 | let doc = result.map_err(|e| { |
1089 | 0 | make_err!( |
1090 | 0 | Code::Internal, |
1091 | | "Error reading document in search_by_index_prefix: {e}" |
1092 | | ) |
1093 | 0 | })?; |
1094 | | |
1095 | 0 | let data = match doc.get(DATA_FIELD) { |
1096 | 0 | Some(Bson::Binary(binary)) => Bytes::from(binary.bytes.clone()), |
1097 | | _ => { |
1098 | 0 | return Err(make_err!( |
1099 | 0 | Code::Internal, |
1100 | 0 | "Missing or invalid data field in search_by_index_prefix" |
1101 | 0 | )); |
1102 | | } |
1103 | | }; |
1104 | | |
1105 | 0 | let version = if <K as SchedulerIndexProvider>::Versioned::VALUE { |
1106 | 0 | doc.get_i64(VERSION_FIELD).unwrap_or(0) |
1107 | | } else { |
1108 | 0 | 0 |
1109 | | }; |
1110 | | |
1111 | 0 | K::decode(version, data).map_err(|e| { |
1112 | 0 | make_err!( |
1113 | 0 | Code::Internal, |
1114 | | "Failed to decode in search_by_index_prefix: {e}" |
1115 | | ) |
1116 | 0 | }) |
1117 | 0 | })) |
1118 | 0 | } |
1119 | | |
1120 | 0 | async fn get_and_decode<K>( |
1121 | 0 | &self, |
1122 | 0 | key: K, |
1123 | 0 | ) -> Result<Option<<K as SchedulerStoreDecodeTo>::DecodeOutput>, Error> |
1124 | 0 | where |
1125 | 0 | K: SchedulerStoreKeyProvider + SchedulerStoreDecodeTo + Send, |
1126 | 0 | { |
1127 | 0 | let key = key.get_key(); |
1128 | 0 | let encoded_key = self.encode_key(&key); |
1129 | 0 | let filter = doc! { KEY_FIELD: encoded_key.as_ref() }; |
1130 | | |
1131 | 0 | let doc = self |
1132 | 0 | .scheduler_collection |
1133 | 0 | .find_one(filter) |
1134 | 0 | .await |
1135 | 0 | .map_err(|e| { |
1136 | 0 | make_err!( |
1137 | 0 | Code::Internal, |
1138 | | "Failed to find document in get_and_decode: {e}" |
1139 | | ) |
1140 | 0 | })?; |
1141 | | |
1142 | 0 | let Some(doc) = doc else { |
1143 | 0 | return Ok(None); |
1144 | | }; |
1145 | | |
1146 | 0 | let data = match doc.get(DATA_FIELD) { |
1147 | 0 | Some(Bson::Binary(binary)) => Bytes::from(binary.bytes.clone()), |
1148 | 0 | _ => return Ok(None), |
1149 | | }; |
1150 | | |
1151 | 0 | let version = if <K as SchedulerStoreKeyProvider>::Versioned::VALUE { |
1152 | 0 | doc.get_i64(VERSION_FIELD).unwrap_or(0) |
1153 | | } else { |
1154 | 0 | 0 |
1155 | | }; |
1156 | | |
1157 | 0 | Ok(Some(K::decode(version, data).map_err(|e| { |
1158 | 0 | make_err!(Code::Internal, "Failed to decode in get_and_decode: {e}") |
1159 | 0 | })?)) |
1160 | 0 | } |
1161 | | } |