Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-store/src/mongo_store.rs
Line
Count
Source
1
// Copyright 2025 Trace Machina, Inc. 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
17
    pub async fn new(mut spec: ExperimentalMongoSpec) -> Result<Arc<Self>, Error> {
120
        // Set defaults
121
17
        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
16
        }
127
128
16
        if spec.database.is_empty() {
129
1
            spec.database = DEFAULT_DB_NAME.to_string();
130
15
        }
131
132
16
        if spec.cas_collection.is_empty() {
133
1
            spec.cas_collection = DEFAULT_COLLECTION_NAME.to_string();
134
15
        }
135
136
16
        if spec.scheduler_collection.is_empty() {
137
1
            spec.scheduler_collection = DEFAULT_SCHEDULER_COLLECTION_NAME.to_string();
138
15
        }
139
140
16
        if spec.read_chunk_size == 0 {
141
1
            spec.read_chunk_size = DEFAULT_READ_CHUNK_SIZE;
142
15
        }
143
144
16
        if spec.connection_timeout_ms == 0 {
145
1
            spec.connection_timeout_ms = DEFAULT_CONNECTION_TIMEOUT_MS;
146
15
        }
147
148
16
        if spec.command_timeout_ms == 0 {
149
1
            spec.command_timeout_ms = DEFAULT_COMMAND_TIMEOUT_MS;
150
15
        }
151
152
16
        if spec.max_concurrent_uploads != 0 {
153
15
            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
16
        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
15
        }
166
167
        // Configure client options
168
15
        let 
mut client_options14
= ClientOptions::parse(&spec.connection_string)
169
15
            .await
170
15
            .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
14
        client_options.server_selection_timeout =
178
14
            Some(Duration::from_millis(spec.connection_timeout_ms));
179
14
        client_options.connect_timeout = Some(Duration::from_millis(spec.connection_timeout_ms));
180
181
        // Set write concern if specified
182
14
        if let Some(
w13
) = spec.write_concern_w {
183
            // Parse write concern w - can be a number or string like "majority"
184
13
            let w_value = if let Ok(
num0
) = w.parse::<u32>() {
185
0
                Some(num.into())
186
            } else {
187
13
                Some(w.into())
188
            };
189
190
            // Build write concern based on which options are set
191
13
            let write_concern = match (w_value, spec.write_concern_j, spec.write_concern_timeout_ms)
192
            {
193
13
                (Some(w), Some(j), Some(timeout)) => WriteConcern::builder()
194
13
                    .w(Some(w))
195
13
                    .journal(j)
196
13
                    .w_timeout(Some(Duration::from_millis(u64::from(timeout))))
197
13
                    .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
13
            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
13
        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
13
        let database = client.database(&spec.database);
225
13
        let cas_collection = database.collection::<Document>(&spec.cas_collection);
226
13
        let scheduler_collection = database.collection::<Document>(&spec.scheduler_collection);
227
228
        // Create indexes
229
13
        Self::create_indexes(&cas_collection, &scheduler_collection).await
?0
;
230
231
13
        let store = Self {
232
13
            client,
233
13
            database,
234
13
            cas_collection,
235
13
            scheduler_collection,
236
13
            key_prefix: spec.key_prefix.clone().unwrap_or_default(),
237
13
            read_chunk_size: spec.read_chunk_size,
238
13
            enable_change_streams: spec.enable_change_streams,
239
13
            subscription_manager: Mutex::new(None),
240
13
            request_permits: Arc::new(Semaphore::new(
241
13
                spec.max_requests.unwrap_or(Semaphore::MAX_PERMITS),
242
13
            )),
243
13
            waiting_permits: Arc::new(AtomicUsize::new(0)),
244
13
        };
245
246
13
        Ok(Arc::new(store))
247
17
    }
248
249
    /// Create necessary indexes for efficient operations.
250
13
    async fn create_indexes(
251
13
        cas_collection: &Collection<Document>,
252
13
        _scheduler_collection: &Collection<Document>,
253
13
    ) -> Result<(), Error> {
254
        // CAS collection indexes
255
13
        cas_collection
256
13
            .create_index(IndexModel::builder().keys(doc! { SIZE_FIELD: 1 }).build())
257
13
            .await
258
13
            .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
13
        Ok(())
268
13
    }
269
270
    /// Encode a [`StoreKey`] so it can be sent to `MongoDB`.
271
64
    fn encode_key<'a>(&self, key: &'a StoreKey<'a>) -> Cow<'a, str> {
272
64
        let key_body = key.as_str();
273
64
        if self.key_prefix.is_empty() {
274
64
            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
64
    }
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
58
    async fn acquire_permit(&self) -> Result<SemaphorePermit<'_>, Error> {
301
58
        let waiting = self.waiting_permits.fetch_add(1, Ordering::Relaxed);
302
303
58
        if waiting > 0 && 
waiting0
.
is_multiple_of0
(100) {
304
0
            info!(waiting, "Number of waiting permits for Mongo");
305
        } else {
306
58
            trace!(waiting, "Number of waiting permits for Mongo");
307
        }
308
58
        let permit = self.request_permits.acquire().await;
309
58
        self.waiting_permits.fetch_sub(1, Ordering::Relaxed);
310
58
        Ok(permit
?0
)
311
58
    }
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
25
    ) -> 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
        loop {
490
            let chunk = reader.recv().await?;
491
            if chunk.is_empty() {
492
                break; // Empty chunk signals EOF
493
            }
494
            data.extend_from_slice(&chunk);
495
        }
496
497
        let size = data.len().try_into().unwrap_or(i64::MAX);
498
        upload_size.validate_size(data.len() as u64)?;
499
500
        // Create document
501
        let doc = doc! {
502
            KEY_FIELD: encoded_key.as_ref(),
503
            DATA_FIELD: Bson::Binary(mongodb::bson::Binary {
504
                subtype: mongodb::bson::spec::BinarySubtype::Generic,
505
                bytes: data,
506
            }),
507
            SIZE_FIELD: size,
508
        };
509
510
        let semaphore = self.acquire_permit().await?;
511
512
        // Upsert the document
513
        self.cas_collection
514
            .update_one(
515
                doc! { KEY_FIELD: encoded_key.as_ref() },
516
                doc! { "$set": doc },
517
            )
518
            .upsert(true)
519
            .await
520
0
            .map_err(|e| make_err!(Code::Internal, "Failed to update document in MongoDB: {e}"))?;
521
522
        drop(semaphore);
523
524
        Ok(size.try_into().unwrap_or(0))
525
25
    }
526
527
    async fn get_part(
528
        self: Pin<&Self>,
529
        key: StoreKey<'_>,
530
        writer: &mut DropCloserWriteHalf,
531
        offset: u64,
532
        length: Option<u64>,
533
22
    ) -> Result<(), Error> {
534
        // Handle zero digest
535
        if is_zero_digest(key.borrow()) {
536
0
            return writer.send_eof().map_err(|e| {
537
0
                make_err!(
538
0
                    Code::Internal,
539
                    "Failed to send zero EOF in mongo store get_part: {e}"
540
                )
541
0
            });
542
        }
543
544
        let encoded_key = self.encode_key(&key);
545
        let filter = doc! { KEY_FIELD: encoded_key.as_ref() };
546
547
        let semaphore = self.acquire_permit().await?;
548
549
        let doc = self
550
            .cas_collection
551
            .find_one(filter)
552
            .await
553
0
            .map_err(|e| make_err!(Code::Internal, "Failed to find document in get_part: {e}"))?
554
1
            .ok_or_else(|| {
555
1
                make_err!(
556
1
                    Code::NotFound,
557
                    "Data not found in MongoDB store for digest: {key:?}"
558
                )
559
1
            })?;
560
561
        let data = match doc.get(DATA_FIELD) {
562
            Some(Bson::Binary(binary)) => &binary.bytes,
563
            _ => {
564
                return Err(make_err!(
565
                    Code::Internal,
566
                    "Invalid data field in MongoDB document"
567
                ));
568
            }
569
        };
570
571
        let offset = usize::try_from(offset).unwrap_or(usize::MAX);
572
        let data_len = data.len();
573
574
        if offset > data_len {
575
0
            return writer.send_eof().map_err(|e| {
576
0
                make_err!(
577
0
                    Code::Internal,
578
                    "Failed to send EOF in mongo store get_part: {e}"
579
                )
580
0
            });
581
        }
582
583
        let end = if let Some(len) = length {
584
            cmp::min(
585
                offset.saturating_add(usize::try_from(len).unwrap_or(usize::MAX)),
586
                data_len,
587
            )
588
        } else {
589
            data_len
590
        };
591
592
        if offset < end {
593
            let chunk = &data[offset..end];
594
595
            // Send data in chunks
596
            for chunk_data in chunk.chunks(self.read_chunk_size) {
597
0
                writer.send(chunk_data.to_vec().into()).await.map_err(|e| {
598
0
                    make_err!(
599
0
                        Code::Internal,
600
                        "Failed to write data in ExperimentalMongoStore::get_part: {e}"
601
                    )
602
0
                })?;
603
            }
604
        }
605
606
        drop(semaphore);
607
608
0
        writer.send_eof().map_err(|e| {
609
0
            make_err!(
610
0
                Code::Internal,
611
                "Failed to write EOF in mongo store get_part: {e}"
612
            )
613
0
        })
614
22
    }
615
616
0
    fn inner_store(&self, _digest: Option<StoreKey>) -> &dyn StoreDriver {
617
0
        self
618
0
    }
619
620
0
    fn as_any(&self) -> &(dyn core::any::Any + Sync + Send) {
621
0
        self
622
0
    }
623
624
0
    fn as_any_arc(self: Arc<Self>) -> Arc<dyn core::any::Any + Sync + Send> {
625
0
        self
626
0
    }
627
628
0
    fn register_health(self: Arc<Self>, registry: &mut HealthRegistryBuilder) {
629
0
        registry.register_indicator(self);
630
0
    }
631
632
0
    fn register_remove_callback(self: Arc<Self>, _callback: RemoveCallback) -> Result<(), Error> {
633
        // drop because we don't remove anything from Mongo
634
0
        Ok(())
635
0
    }
636
}
637
638
#[async_trait]
639
impl HealthStatusIndicator for ExperimentalMongoStore {
640
0
    fn get_name(&self) -> &'static str {
641
0
        "ExperimentalMongoStore"
642
0
    }
643
644
0
    async fn check_health(&self, namespace: Cow<'static, str>) -> HealthStatus {
645
        // Note we do not acquire a request_permit here, as the health check needs to always go through
646
        // even if everything else is fully loaded
647
        match self.database.run_command(doc! { "ping": 1 }).await {
648
            Ok(_) => HealthStatus::new_ok(self, "Connection healthy".into()),
649
            Err(e) => HealthStatus::new_failed(
650
                self,
651
                format!("{namespace} - MongoDB connection error: {e}").into(),
652
            ),
653
        }
654
0
    }
655
}
656
657
// -------------------------------------------------------------------
658
//
659
// `ExperimentalMongoDB` scheduler implementation. Likely to change
660
// -------------------------------------------------------------------
661
662
/// An individual subscription to a key in `ExperimentalMongoDB`.
663
#[derive(Debug)]
664
pub struct ExperimentalMongoSubscription {
665
    receiver: Option<watch::Receiver<String>>,
666
    weak_subscribed_keys: Weak<RwLock<StringPatriciaMap<ExperimentalMongoSubscriptionPublisher>>>,
667
}
668
669
impl SchedulerSubscription for ExperimentalMongoSubscription {
670
0
    async fn changed(&mut self) -> Result<(), Error> {
671
0
        let receiver = self.receiver.as_mut().ok_or_else(|| {
672
0
            make_err!(
673
0
                Code::Internal,
674
                "In ExperimentalMongoSubscription::changed::as_mut"
675
            )
676
0
        })?;
677
0
        receiver.changed().await.map_err(|err| {
678
0
            Error::from_std_err(Code::Internal, &err)
679
0
                .append("In ExperimentalMongoSubscription::changed::changed")
680
0
        })
681
0
    }
682
}
683
684
impl Drop for ExperimentalMongoSubscription {
685
0
    fn drop(&mut self) {
686
0
        let Some(receiver) = self.receiver.take() else {
687
0
            warn!("ExperimentalMongoSubscription has already been dropped, nothing to do.");
688
0
            return;
689
        };
690
0
        let key = receiver.borrow().clone();
691
0
        drop(receiver);
692
693
0
        let Some(subscribed_keys) = self.weak_subscribed_keys.upgrade() else {
694
0
            return;
695
        };
696
0
        let mut subscribed_keys = subscribed_keys.write();
697
0
        let Some(value) = subscribed_keys.get(&key) else {
698
0
            error!(
699
                "Key {key} was not found in subscribed keys when checking if it should be removed."
700
            );
701
0
            return;
702
        };
703
704
0
        if value.receiver_count() == 0 {
705
0
            subscribed_keys.remove(key);
706
0
        }
707
0
    }
708
}
709
710
/// A publisher for a key in `ExperimentalMongoDB`.
711
#[derive(Debug)]
712
struct ExperimentalMongoSubscriptionPublisher {
713
    sender: Mutex<watch::Sender<String>>,
714
}
715
716
impl ExperimentalMongoSubscriptionPublisher {
717
0
    fn new(
718
0
        key: String,
719
0
        weak_subscribed_keys: Weak<RwLock<StringPatriciaMap<Self>>>,
720
0
    ) -> (Self, ExperimentalMongoSubscription) {
721
0
        let (sender, receiver) = watch::channel(key);
722
0
        let publisher = Self {
723
0
            sender: Mutex::new(sender),
724
0
        };
725
0
        let subscription = ExperimentalMongoSubscription {
726
0
            receiver: Some(receiver),
727
0
            weak_subscribed_keys,
728
0
        };
729
0
        (publisher, subscription)
730
0
    }
731
732
0
    fn subscribe(
733
0
        &self,
734
0
        weak_subscribed_keys: Weak<RwLock<StringPatriciaMap<Self>>>,
735
0
    ) -> ExperimentalMongoSubscription {
736
0
        let receiver = self.sender.lock().subscribe();
737
0
        ExperimentalMongoSubscription {
738
0
            receiver: Some(receiver),
739
0
            weak_subscribed_keys,
740
0
        }
741
0
    }
742
743
0
    fn receiver_count(&self) -> usize {
744
0
        self.sender.lock().receiver_count()
745
0
    }
746
747
0
    fn notify(&self) {
748
0
        self.sender.lock().send_modify(|_| {});
749
0
    }
750
}
751
752
#[derive(Debug)]
753
pub struct ExperimentalMongoSubscriptionManager {
754
    subscribed_keys: Arc<RwLock<StringPatriciaMap<ExperimentalMongoSubscriptionPublisher>>>,
755
    _subscription_spawn: JoinHandleDropGuard<()>,
756
}
757
758
impl ExperimentalMongoSubscriptionManager {
759
0
    pub fn new(database: Database, collection_name: String, key_prefix: String) -> Self {
760
0
        let subscribed_keys = Arc::new(RwLock::new(StringPatriciaMap::new()));
761
0
        let subscribed_keys_weak = Arc::downgrade(&subscribed_keys);
762
763
        Self {
764
0
            subscribed_keys,
765
0
            _subscription_spawn: spawn!("mongo_subscribe_spawn", async move {
766
0
                let collection = database.collection::<Document>(&collection_name);
767
768
                loop {
769
                    // Try to create change stream
770
0
                    let change_stream_result = collection.watch().await;
771
772
0
                    match change_stream_result {
773
0
                        Ok(mut change_stream) => {
774
0
                            info!("MongoDB change stream connected");
775
776
0
                            while let Some(event_result) = change_stream.next().await {
777
0
                                match event_result {
778
0
                                    Ok(event) => {
779
                                        use mongodb::change_stream::event::OperationType;
780
0
                                        match event.operation_type {
781
                                            OperationType::Insert
782
                                            | OperationType::Update
783
                                            | OperationType::Replace
784
                                            | OperationType::Delete => {
785
0
                                                if let Some(doc_key) = event.document_key
786
0
                                                    && let Ok(key) = doc_key.get_str(KEY_FIELD)
787
                                                {
788
                                                    // Remove prefix if present
789
0
                                                    let key = if key_prefix.is_empty() {
790
0
                                                        key
791
                                                    } else {
792
0
                                                        key.strip_prefix(&key_prefix).unwrap_or(key)
793
                                                    };
794
795
0
                                                    let Some(subscribed_keys) =
796
0
                                                        subscribed_keys_weak.upgrade()
797
                                                    else {
798
0
                                                        warn!(
799
                                                            "Parent dropped, exiting ExperimentalMongoSubscriptionManager"
800
                                                        );
801
0
                                                        return;
802
                                                    };
803
804
0
                                                    let subscribed_keys_mux =
805
0
                                                        subscribed_keys.read();
806
0
                                                    subscribed_keys_mux
807
0
                                                                .common_prefix_values(key)
808
0
                                                                .for_each(ExperimentalMongoSubscriptionPublisher::notify);
809
0
                                                }
810
                                            }
811
0
                                            _ => {}
812
                                        }
813
                                    }
814
0
                                    Err(e) => {
815
0
                                        error!("Error in change stream: {e}");
816
0
                                        break;
817
                                    }
818
                                }
819
                            }
820
                        }
821
0
                        Err(e) => {
822
0
                            warn!("Failed to create change stream: {e}. Will retry in 5 seconds.");
823
                        }
824
                    }
825
826
                    // Check if parent is still alive
827
0
                    if subscribed_keys_weak.upgrade().is_none() {
828
0
                        warn!("Parent dropped, exiting ExperimentalMongoSubscriptionManager");
829
0
                        return;
830
0
                    }
831
832
                    // Sleep before retry
833
0
                    sleep(Duration::from_secs(5)).await;
834
835
                    // Notify all subscribers on reconnection
836
0
                    if let Some(subscribed_keys) = subscribed_keys_weak.upgrade() {
837
0
                        let subscribed_keys_mux = subscribed_keys.read();
838
0
                        for publisher in subscribed_keys_mux.values() {
839
0
                            publisher.notify();
840
0
                        }
841
0
                    }
842
                }
843
0
            }),
844
        }
845
0
    }
846
}
847
848
impl SchedulerSubscriptionManager for ExperimentalMongoSubscriptionManager {
849
    type Subscription = ExperimentalMongoSubscription;
850
851
0
    fn subscribe<K>(&self, key: K) -> Result<Self::Subscription, Error>
852
0
    where
853
0
        K: SchedulerStoreKeyProvider,
854
    {
855
0
        let weak_subscribed_keys = Arc::downgrade(&self.subscribed_keys);
856
0
        let mut subscribed_keys = self.subscribed_keys.write();
857
0
        let key = key.get_key();
858
0
        let key_str = key.as_str();
859
860
0
        let mut subscription = if let Some(publisher) = subscribed_keys.get(&key_str) {
861
0
            publisher.subscribe(weak_subscribed_keys)
862
        } else {
863
0
            let (publisher, subscription) = ExperimentalMongoSubscriptionPublisher::new(
864
0
                key_str.to_string(),
865
0
                weak_subscribed_keys,
866
0
            );
867
0
            subscribed_keys.insert(key_str, publisher);
868
0
            subscription
869
        };
870
871
0
        subscription
872
0
            .receiver
873
0
            .as_mut()
874
0
            .ok_or_else(|| {
875
0
                make_err!(
876
0
                    Code::Internal,
877
                    "Receiver should be set in ExperimentalMongoSubscriptionManager::subscribe"
878
                )
879
0
            })?
880
0
            .mark_changed();
881
882
0
        Ok(subscription)
883
0
    }
884
885
0
    fn is_reliable() -> bool {
886
0
        true
887
0
    }
888
}
889
890
impl SchedulerStore for ExperimentalMongoStore {
891
    type SubscriptionManager = ExperimentalMongoSubscriptionManager;
892
893
0
    fn subscription_manager(
894
0
        &self,
895
0
    ) -> impl Future<Output = Result<Arc<ExperimentalMongoSubscriptionManager>, Error>> {
896
0
        let mut subscription_manager = self.subscription_manager.lock();
897
0
        std::future::ready(if let Some(subscription_manager) = &*subscription_manager {
898
0
            Ok(subscription_manager.clone())
899
0
        } else if self.enable_change_streams {
900
0
            let sub = Arc::new(ExperimentalMongoSubscriptionManager::new(
901
0
                self.database.clone(),
902
0
                self.scheduler_collection.name().to_string(),
903
0
                self.key_prefix.clone(),
904
            ));
905
0
            *subscription_manager = Some(sub.clone());
906
0
            Ok(sub)
907
        } else {
908
0
            Err(make_input_err!(
909
0
                "ExperimentalMongoStore must have change streams enabled for scheduler subscriptions"
910
0
            ))
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
            let direction = if K::SORT_DESCENDING { -1 } else { 1 };
1071
0
            FindOptions::builder()
1072
0
                .sort(doc! { sort_key: direction })
1073
0
                .build()
1074
        } else {
1075
0
            FindOptions::default()
1076
        };
1077
1078
0
        let cursor = self
1079
0
            .scheduler_collection
1080
0
            .find(filter)
1081
0
            .with_options(find_options)
1082
0
            .await
1083
0
            .map_err(|e| {
1084
0
                make_err!(
1085
0
                    Code::Internal,
1086
                    "Failed to create cursor in search_by_index_prefix: {e}"
1087
                )
1088
0
            })?;
1089
1090
0
        Ok(cursor.map(move |result| {
1091
0
            let doc = result.map_err(|e| {
1092
0
                make_err!(
1093
0
                    Code::Internal,
1094
                    "Error reading document in search_by_index_prefix: {e}"
1095
                )
1096
0
            })?;
1097
1098
0
            let data = match doc.get(DATA_FIELD) {
1099
0
                Some(Bson::Binary(binary)) => Bytes::from(binary.bytes.clone()),
1100
                _ => {
1101
0
                    return Err(make_err!(
1102
0
                        Code::Internal,
1103
0
                        "Missing or invalid data field in search_by_index_prefix"
1104
0
                    ));
1105
                }
1106
            };
1107
1108
0
            let version = if <K as SchedulerIndexProvider>::Versioned::VALUE {
1109
0
                doc.get_i64(VERSION_FIELD).unwrap_or(0)
1110
            } else {
1111
0
                0
1112
            };
1113
1114
0
            K::decode(version, data).map_err(|e| {
1115
0
                make_err!(
1116
0
                    Code::Internal,
1117
                    "Failed to decode in search_by_index_prefix: {e}"
1118
                )
1119
0
            })
1120
0
        }))
1121
0
    }
1122
1123
0
    async fn count_by_index_prefix<K>(&self, index: K) -> Result<u64, Error>
1124
0
    where
1125
0
        K: SchedulerIndexProvider + Send,
1126
0
    {
1127
0
        let index_value = index.index_value();
1128
0
        let filter = doc! {
1129
            K::INDEX_NAME: {
1130
0
                "$regex": format!("^{}", regex::escape(index_value.as_ref())),
1131
            }
1132
        };
1133
        // Counting is a read-only observer, so unlike search_by_index_prefix it
1134
        // does not create the index.
1135
0
        self.scheduler_collection
1136
0
            .count_documents(filter)
1137
0
            .await
1138
0
            .map_err(|e| {
1139
0
                make_err!(
1140
0
                    Code::Internal,
1141
                    "Failed to count in count_by_index_prefix: {e}"
1142
                )
1143
0
            })
1144
0
    }
1145
1146
0
    async fn get_and_decode<K>(
1147
0
        &self,
1148
0
        key: K,
1149
0
    ) -> Result<Option<<K as SchedulerStoreDecodeTo>::DecodeOutput>, Error>
1150
0
    where
1151
0
        K: SchedulerStoreKeyProvider + SchedulerStoreDecodeTo + Send,
1152
0
    {
1153
0
        let key = key.get_key();
1154
0
        let encoded_key = self.encode_key(&key);
1155
0
        let filter = doc! { KEY_FIELD: encoded_key.as_ref() };
1156
1157
0
        let doc = self
1158
0
            .scheduler_collection
1159
0
            .find_one(filter)
1160
0
            .await
1161
0
            .map_err(|e| {
1162
0
                make_err!(
1163
0
                    Code::Internal,
1164
                    "Failed to find document in get_and_decode: {e}"
1165
                )
1166
0
            })?;
1167
1168
0
        let Some(doc) = doc else {
1169
0
            return Ok(None);
1170
        };
1171
1172
0
        let data = match doc.get(DATA_FIELD) {
1173
0
            Some(Bson::Binary(binary)) => Bytes::from(binary.bytes.clone()),
1174
0
            _ => return Ok(None),
1175
        };
1176
1177
0
        let version = if <K as SchedulerStoreKeyProvider>::Versioned::VALUE {
1178
0
            doc.get_i64(VERSION_FIELD).unwrap_or(0)
1179
        } else {
1180
0
            0
1181
        };
1182
1183
0
        Ok(Some(K::decode(version, data).map_err(|e| {
1184
0
            make_err!(Code::Internal, "Failed to decode in get_and_decode: {e}")
1185
0
        })?))
1186
0
    }
1187
}