1use super::*;
2
3#[derive(Debug, Clone)]
4struct MetricsRetentionRollupPolicy {
5 target: String,
6 aggregation: String,
7 bucket_ns: u64,
8}
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
11struct MetricsRollupKey {
12 metric: String,
13 bucket_ns: u64,
14 tags: Vec<(String, String)>,
15}
16
17#[derive(Debug, Clone)]
18struct MetricsRollupAccumulator {
19 count: u64,
20 sum: f64,
21 min: f64,
22 max: f64,
23}
24
25impl MetricsRollupAccumulator {
26 fn new(value: f64) -> Self {
27 Self {
28 count: 1,
29 sum: value,
30 min: value,
31 max: value,
32 }
33 }
34
35 fn push(&mut self, value: f64) {
36 self.count = self.count.saturating_add(1);
37 self.sum += value;
38 self.min = self.min.min(value);
39 self.max = self.max.max(value);
40 }
41
42 fn value(&self, aggregation: &str) -> f64 {
43 match aggregation {
44 "sum" => self.sum,
45 "min" => self.min,
46 "max" => self.max,
47 "count" => self.count as f64,
48 _ => self.sum / self.count.max(1) as f64,
49 }
50 }
51}
52
53fn metrics_rollup_policies_for_retention(
54 contract: &crate::physical::CollectionContract,
55) -> Vec<MetricsRetentionRollupPolicy> {
56 contract
57 .metrics_rollup_policies
58 .iter()
59 .filter_map(|spec| {
60 let parsed = crate::storage::timeseries::retention::DownsamplePolicy::parse(spec)?;
61 if parsed.source != "raw"
62 || !matches!(
63 parsed.aggregation.as_str(),
64 "avg" | "sum" | "min" | "max" | "count"
65 )
66 {
67 return None;
68 }
69 Some(MetricsRetentionRollupPolicy {
70 target: parsed.target,
71 aggregation: parsed.aggregation,
72 bucket_ns: parsed.bucket_ns,
73 })
74 })
75 .collect()
76}
77
78fn metrics_rollup_collection_for_retention(raw_collection: &str, target: &str) -> String {
79 let sanitized = target
80 .chars()
81 .map(|ch| {
82 if ch.is_ascii_alphanumeric() || ch == '_' {
83 ch
84 } else {
85 '_'
86 }
87 })
88 .collect::<String>();
89 format!("red_metrics_rollup_{raw_collection}_{sanitized}")
90}
91
92impl RedDBRuntime {
93 pub fn create_export(&self, name: impl Into<String>) -> RedDBResult<ExportDescriptor> {
94 self.inner
95 .db
96 .create_named_export(name)
97 .map_err(|err| RedDBError::Internal(err.to_string()))
98 }
99
100 pub fn graph_projections(&self) -> RedDBResult<Vec<PhysicalGraphProjection>> {
101 Ok(self.inner.db.declared_graph_projections())
102 }
103
104 pub fn operational_graph_projections(&self) -> Vec<PhysicalGraphProjection> {
105 self.inner.db.operational_graph_projections()
106 }
107
108 pub fn graph_projection_named(&self, name: &str) -> RedDBResult<RuntimeGraphProjection> {
109 let status = self
110 .graph_projection_statuses()
111 .into_iter()
112 .find(|status| status.name == name)
113 .ok_or_else(|| RedDBError::NotFound(name.to_string()))?;
114 if !status.declared {
115 return Err(RedDBError::Catalog(format!(
116 "graph projection '{name}' is not declared"
117 )));
118 }
119 if !status.operational {
120 return Err(RedDBError::Catalog(format!(
121 "graph projection '{name}' is declared but not operationally materialized"
122 )));
123 }
124 if status.lifecycle_state == "stale" {
125 return Err(RedDBError::Catalog(format!(
126 "graph projection '{name}' is stale and must be rematerialized before use"
127 )));
128 }
129 let projection = self
130 .operational_graph_projections()
131 .into_iter()
132 .find(|projection| projection.name == name)
133 .ok_or_else(|| RedDBError::NotFound(name.to_string()))?;
134 Ok(RuntimeGraphProjection {
135 node_labels: (!projection.node_labels.is_empty()).then_some(projection.node_labels),
136 node_types: (!projection.node_types.is_empty()).then_some(projection.node_types),
137 edge_labels: (!projection.edge_labels.is_empty()).then_some(projection.edge_labels),
138 })
139 }
140
141 pub fn save_graph_projection(
142 &self,
143 name: impl Into<String>,
144 projection: RuntimeGraphProjection,
145 source: Option<String>,
146 ) -> RedDBResult<PhysicalGraphProjection> {
147 self.inner
148 .db
149 .save_graph_projection(
150 name,
151 projection.node_labels.unwrap_or_default(),
152 projection.node_types.unwrap_or_default(),
153 projection.edge_labels.unwrap_or_default(),
154 source.unwrap_or_else(|| "runtime".to_string()),
155 )
156 .map_err(|err| RedDBError::Internal(err.to_string()))
157 }
158
159 pub fn materialize_graph_projection(&self, name: &str) -> RedDBResult<PhysicalGraphProjection> {
160 self.inner
161 .db
162 .materialize_graph_projection(name)
163 .map_err(|err| RedDBError::Internal(err.to_string()))?
164 .ok_or_else(|| RedDBError::NotFound(name.to_string()))
165 }
166
167 pub fn mark_graph_projection_materializing(
168 &self,
169 name: &str,
170 ) -> RedDBResult<PhysicalGraphProjection> {
171 self.inner
172 .db
173 .mark_graph_projection_materializing(name)
174 .map_err(|err| RedDBError::Internal(err.to_string()))?
175 .ok_or_else(|| RedDBError::NotFound(name.to_string()))
176 }
177
178 pub fn fail_graph_projection(&self, name: &str) -> RedDBResult<PhysicalGraphProjection> {
179 self.inner
180 .db
181 .fail_graph_projection(name)
182 .map_err(|err| RedDBError::Internal(err.to_string()))?
183 .ok_or_else(|| RedDBError::NotFound(name.to_string()))
184 }
185
186 pub fn mark_graph_projection_stale(&self, name: &str) -> RedDBResult<PhysicalGraphProjection> {
187 self.inner
188 .db
189 .mark_graph_projection_stale(name)
190 .map_err(|err| RedDBError::Internal(err.to_string()))?
191 .ok_or_else(|| RedDBError::NotFound(name.to_string()))
192 }
193
194 pub fn analytics_jobs(&self) -> RedDBResult<Vec<PhysicalAnalyticsJob>> {
195 Ok(self.inner.db.declared_analytics_jobs())
196 }
197
198 pub fn operational_analytics_jobs(&self) -> Vec<PhysicalAnalyticsJob> {
199 self.inner.db.operational_analytics_jobs()
200 }
201
202 pub fn save_analytics_job(
203 &self,
204 kind: impl Into<String>,
205 projection_name: Option<String>,
206 metadata: std::collections::BTreeMap<String, String>,
207 ) -> RedDBResult<PhysicalAnalyticsJob> {
208 self.inner
209 .db
210 .save_analytics_job(kind, projection_name, metadata)
211 .map_err(|err| RedDBError::Internal(err.to_string()))
212 }
213
214 pub fn start_analytics_job(
215 &self,
216 kind: impl Into<String>,
217 projection_name: Option<String>,
218 metadata: std::collections::BTreeMap<String, String>,
219 ) -> RedDBResult<PhysicalAnalyticsJob> {
220 if let Some(projection_name) = projection_name.as_deref() {
221 let status = self
222 .graph_projection_statuses()
223 .into_iter()
224 .find(|status| status.name == projection_name)
225 .ok_or_else(|| RedDBError::NotFound(projection_name.to_string()))?;
226 if !status.declared {
227 return Err(RedDBError::Catalog(format!(
228 "graph projection '{projection_name}' is not declared"
229 )));
230 }
231 if !status.operational {
232 return Err(RedDBError::Catalog(format!(
233 "graph projection '{projection_name}' is declared but not operationally materialized"
234 )));
235 }
236 if status.lifecycle_state == "stale" {
237 return Err(RedDBError::Catalog(format!(
238 "graph projection '{projection_name}' is stale and must be rematerialized before analytics jobs can start against it"
239 )));
240 }
241 }
242 self.inner
243 .db
244 .start_analytics_job(kind, projection_name, metadata)
245 .map_err(|err| RedDBError::Internal(err.to_string()))
246 }
247
248 pub fn queue_analytics_job(
249 &self,
250 kind: impl Into<String>,
251 projection_name: Option<String>,
252 metadata: std::collections::BTreeMap<String, String>,
253 ) -> RedDBResult<PhysicalAnalyticsJob> {
254 if let Some(projection_name) = projection_name.as_deref() {
255 let status = self
256 .graph_projection_statuses()
257 .into_iter()
258 .find(|status| status.name == projection_name)
259 .ok_or_else(|| RedDBError::NotFound(projection_name.to_string()))?;
260 if !status.declared {
261 return Err(RedDBError::Catalog(format!(
262 "graph projection '{projection_name}' is not declared"
263 )));
264 }
265 if !status.operational {
266 return Err(RedDBError::Catalog(format!(
267 "graph projection '{projection_name}' is declared but not operationally materialized"
268 )));
269 }
270 if status.lifecycle_state == "stale" {
271 return Err(RedDBError::Catalog(format!(
272 "graph projection '{projection_name}' is stale and must be rematerialized before analytics jobs can be queued against it"
273 )));
274 }
275 }
276 self.inner
277 .db
278 .queue_analytics_job(kind, projection_name, metadata)
279 .map_err(|err| RedDBError::Internal(err.to_string()))
280 }
281
282 pub fn fail_analytics_job(
283 &self,
284 kind: impl Into<String>,
285 projection_name: Option<String>,
286 metadata: std::collections::BTreeMap<String, String>,
287 ) -> RedDBResult<PhysicalAnalyticsJob> {
288 self.inner
289 .db
290 .fail_analytics_job(kind, projection_name, metadata)
291 .map_err(|err| RedDBError::Internal(err.to_string()))
292 }
293
294 pub fn mark_analytics_job_stale(
295 &self,
296 kind: impl Into<String>,
297 projection_name: Option<String>,
298 metadata: std::collections::BTreeMap<String, String>,
299 ) -> RedDBResult<PhysicalAnalyticsJob> {
300 self.inner
301 .db
302 .mark_analytics_job_stale(kind, projection_name, metadata)
303 .map_err(|err| RedDBError::Internal(err.to_string()))
304 }
305
306 pub fn complete_analytics_job(
307 &self,
308 kind: impl Into<String>,
309 projection_name: Option<String>,
310 metadata: std::collections::BTreeMap<String, String>,
311 ) -> RedDBResult<PhysicalAnalyticsJob> {
312 self.inner
313 .db
314 .record_analytics_job(kind, projection_name, metadata)
315 .map_err(|err| RedDBError::Internal(err.to_string()))
316 }
317
318 pub fn record_analytics_job(
319 &self,
320 kind: impl Into<String>,
321 projection_name: Option<String>,
322 metadata: std::collections::BTreeMap<String, String>,
323 ) -> RedDBResult<PhysicalAnalyticsJob> {
324 if let Some(projection_name) = projection_name.as_deref() {
325 let status = self
326 .graph_projection_statuses()
327 .into_iter()
328 .find(|status| status.name == projection_name)
329 .ok_or_else(|| RedDBError::NotFound(projection_name.to_string()))?;
330 if !status.declared {
331 return Err(RedDBError::Catalog(format!(
332 "graph projection '{projection_name}' is not declared"
333 )));
334 }
335 if !status.operational {
336 return Err(RedDBError::Catalog(format!(
337 "graph projection '{projection_name}' is declared but not operationally materialized"
338 )));
339 }
340 if status.lifecycle_state == "stale" {
341 return Err(RedDBError::Catalog(format!(
342 "graph projection '{projection_name}' is stale and must be rematerialized before analytics jobs can complete against it"
343 )));
344 }
345 }
346 self.inner
347 .db
348 .record_analytics_job(kind, projection_name, metadata)
349 .map_err(|err| RedDBError::Internal(err.to_string()))
350 }
351
352 pub fn resolve_graph_projection(
353 &self,
354 projection_name: Option<&str>,
355 inline: Option<RuntimeGraphProjection>,
356 ) -> RedDBResult<Option<RuntimeGraphProjection>> {
357 let named = match projection_name {
358 Some(name) => Some(self.graph_projection_named(name)?),
359 None => None,
360 };
361 Ok(merge_runtime_projection(named, inline))
362 }
363
364 pub fn apply_retention_policy(&self) -> RedDBResult<()> {
365 self.check_write(crate::runtime::write_gate::WriteKind::Maintenance)?;
366 let now_ms = current_unix_ms_u64();
367 self.retire_expired_append_only_segments(now_ms, usize::MAX)?;
368 let expired = self
369 .inner
370 .db
371 .ttl_expired_entities_now()
372 .map_err(|err| RedDBError::Internal(err.to_string()))?;
373 let store = self.inner.db.store();
374 for (collection, id) in expired {
375 let deleted = store
376 .delete(&collection, id)
377 .map_err(|err| RedDBError::Internal(err.to_string()))?;
378 if deleted {
379 store.context_index().remove_entity(id);
380 self.cdc_emit(
381 crate::replication::cdc::ChangeOperation::Delete,
382 &collection,
383 id.raw(),
384 "entity",
385 );
386 }
387 }
388 self.inner
389 .db
390 .enforce_retention_policy()
391 .map_err(|err| RedDBError::Internal(err.to_string()))?;
392 self.enforce_metrics_raw_retention()?;
393 self.invalidate_result_cache();
394 Ok(())
395 }
396
397 pub(crate) fn retire_expired_append_only_segments(
398 &self,
399 now_ms: u64,
400 max_segments: usize,
401 ) -> RedDBResult<u64> {
402 if max_segments == 0 {
403 return Ok(0);
404 }
405 let Some(path) = self.inner.db.path() else {
406 return Ok(0);
407 };
408 let manifest = reddb_file::OperationalManifest::for_db_path(path);
409 let segments = manifest
410 .append_only_segments()
411 .map_err(|err| RedDBError::Internal(err.to_string()))?;
412 if segments.is_empty() {
413 return Ok(0);
414 }
415
416 let mut total_retired = 0u64;
417 for contract in self
418 .inner
419 .db
420 .collection_contracts()
421 .into_iter()
422 .filter(|contract| contract.append_only)
423 {
424 let Some(retention_ms) = contract.retention_duration_ms else {
425 continue;
426 };
427 let cutoff = (now_ms as i64).saturating_sub(retention_ms as i64);
428 let mut retired_for_collection = 0u64;
429 for segment in segments
430 .iter()
431 .filter(|segment| segment.collection == contract.name)
432 .filter(|segment| segment.retention_max_ms.is_some_and(|max| max < cutoff))
433 .take(max_segments.saturating_sub(total_retired as usize))
434 {
435 manifest
436 .begin_retire_append_only_segment(&segment.collection, segment.segment_id)
437 .map_err(|err| RedDBError::Internal(err.to_string()))?;
438 if manifest
439 .finish_retire_append_only_segment(&segment.collection, segment.segment_id)
440 .map_err(|err| RedDBError::Internal(err.to_string()))?
441 {
442 retired_for_collection = retired_for_collection.saturating_add(1);
443 }
444 }
445 if retired_for_collection > 0 {
446 self.inner
447 .retention_sweeper
448 .write()
449 .record_segments_retired(&contract.name, retired_for_collection, now_ms);
450 total_retired = total_retired.saturating_add(retired_for_collection);
451 }
452 if total_retired >= max_segments as u64 {
453 break;
454 }
455 }
456 Ok(total_retired)
457 }
458
459 fn enforce_metrics_raw_retention(&self) -> RedDBResult<()> {
460 let now_ns = std::time::SystemTime::now()
461 .duration_since(std::time::UNIX_EPOCH)
462 .unwrap_or_default()
463 .as_nanos()
464 .min(u128::from(u64::MAX)) as u64;
465 let store = self.inner.db.store();
466
467 for contract in self
468 .inner
469 .db
470 .collection_contracts()
471 .into_iter()
472 .filter(|contract| contract.declared_model == crate::catalog::CollectionModel::Metrics)
473 {
474 let Some(raw_retention_ms) = contract.metrics_raw_retention_ms else {
475 continue;
476 };
477 let cutoff_ns = now_ns.saturating_sub(raw_retention_ms.saturating_mul(1_000_000));
478 let Some(manager) = store.get_collection(&contract.name) else {
479 continue;
480 };
481 let expired = manager.query_all(|entity| match &entity.data {
482 crate::storage::EntityData::TimeSeries(point) => point.timestamp_ns < cutoff_ns,
483 _ => false,
484 });
485 self.materialize_metrics_rollups_for_retention(&contract, &expired)?;
486 for entity in expired {
487 let deleted = store
488 .delete(&contract.name, entity.id)
489 .map_err(|err| RedDBError::Internal(err.to_string()))?;
490 if deleted {
491 store.context_index().remove_entity(entity.id);
492 self.cdc_emit(
493 crate::replication::cdc::ChangeOperation::Delete,
494 &contract.name,
495 entity.id.raw(),
496 "entity",
497 );
498 }
499 }
500 }
501
502 Ok(())
503 }
504
505 fn materialize_metrics_rollups_for_retention(
506 &self,
507 contract: &crate::physical::CollectionContract,
508 raw_points: &[crate::storage::UnifiedEntity],
509 ) -> RedDBResult<()> {
510 if raw_points.is_empty() {
511 return Ok(());
512 }
513 let policies = metrics_rollup_policies_for_retention(contract);
514 if policies.is_empty() {
515 return Ok(());
516 }
517
518 let store = self.inner.db.store();
519 for policy in policies {
520 let rollup_collection =
521 metrics_rollup_collection_for_retention(&contract.name, &policy.target);
522 if store.get_collection(&rollup_collection).is_none() {
523 store
524 .create_collection(&rollup_collection)
525 .map_err(|err| RedDBError::Internal(err.to_string()))?;
526 }
527
528 let mut buckets = BTreeMap::<MetricsRollupKey, MetricsRollupAccumulator>::new();
529 for entity in raw_points {
530 let crate::storage::EntityData::TimeSeries(point) = &entity.data else {
531 continue;
532 };
533 let bucket_ns = (point.timestamp_ns / policy.bucket_ns) * policy.bucket_ns;
534 let mut tags = point
535 .tags
536 .iter()
537 .map(|(name, value)| (name.clone(), value.clone()))
538 .collect::<Vec<_>>();
539 tags.sort();
540 let key = MetricsRollupKey {
541 metric: point.metric.clone(),
542 bucket_ns,
543 tags,
544 };
545 buckets
546 .entry(key)
547 .and_modify(|acc| acc.push(point.value))
548 .or_insert_with(|| MetricsRollupAccumulator::new(point.value));
549 }
550
551 for (key, accumulator) in buckets {
552 let tags = key.tags.into_iter().collect::<HashMap<_, _>>();
553 if let Some(manager) = store.get_collection(&rollup_collection) {
554 for entity in manager.query_all(|entity| match &entity.data {
555 crate::storage::EntityData::TimeSeries(point) => {
556 point.metric == key.metric
557 && point.timestamp_ns == key.bucket_ns
558 && point.tags == tags
559 }
560 _ => false,
561 }) {
562 store
563 .delete(&rollup_collection, entity.id)
564 .map_err(|err| RedDBError::Internal(err.to_string()))?;
565 }
566 }
567
568 let entity = crate::storage::UnifiedEntity::new(
569 crate::storage::EntityId::new(0),
570 crate::storage::EntityKind::TimeSeriesPoint(Box::new(
571 crate::storage::TimeSeriesPointKind {
572 series: rollup_collection.clone(),
573 metric: key.metric.clone(),
574 },
575 )),
576 crate::storage::EntityData::TimeSeries(crate::storage::TimeSeriesData {
577 metric: key.metric,
578 timestamp_ns: key.bucket_ns,
579 value: accumulator.value(&policy.aggregation),
580 tags,
581 series_id: None,
584 fields: std::collections::HashMap::new(),
585 }),
586 );
587 store
588 .insert_auto(&rollup_collection, entity)
589 .map_err(|err| RedDBError::Internal(err.to_string()))?;
590 }
591 }
592
593 Ok(())
594 }
595
596 pub fn indexes(&self) -> Vec<crate::PhysicalIndexState> {
597 self.inner.db.operational_indexes()
598 }
599
600 pub fn declared_indexes(&self) -> Vec<crate::PhysicalIndexState> {
601 self.inner.db.declared_indexes()
602 }
603
604 pub fn declared_indexes_for_collection(
605 &self,
606 collection: &str,
607 ) -> Vec<crate::PhysicalIndexState> {
608 self.inner
609 .db
610 .declared_indexes()
611 .into_iter()
612 .filter(|index| index.collection.as_deref() == Some(collection))
613 .collect()
614 }
615
616 pub fn index_statuses(&self) -> Vec<crate::catalog::CatalogIndexStatus> {
617 self.inner.db.index_statuses()
618 }
619
620 pub fn graph_projection_statuses(&self) -> Vec<crate::catalog::CatalogGraphProjectionStatus> {
621 self.inner
622 .db
623 .catalog_model_snapshot()
624 .graph_projection_statuses
625 }
626
627 pub fn analytics_job_statuses(&self) -> Vec<crate::catalog::CatalogAnalyticsJobStatus> {
628 self.inner
629 .db
630 .catalog_model_snapshot()
631 .analytics_job_statuses
632 }
633
634 pub fn indexes_for_collection(&self, collection: &str) -> Vec<crate::PhysicalIndexState> {
635 self.inner
636 .db
637 .operational_indexes()
638 .into_iter()
639 .filter(|index| index.collection.as_deref() == Some(collection))
640 .collect()
641 }
642
643 pub fn set_index_enabled(
644 &self,
645 name: &str,
646 enabled: bool,
647 ) -> RedDBResult<crate::PhysicalIndexState> {
648 self.check_write(crate::runtime::write_gate::WriteKind::Maintenance)?;
649 self.inner
650 .db
651 .set_index_enabled(name, enabled)
652 .map_err(|err| RedDBError::Internal(err.to_string()))?
653 .ok_or_else(|| RedDBError::NotFound(name.to_string()))
654 }
655
656 pub fn mark_index_building(&self, name: &str) -> RedDBResult<crate::PhysicalIndexState> {
657 self.inner
658 .db
659 .mark_index_building(name)
660 .map_err(|err| RedDBError::Internal(err.to_string()))?
661 .ok_or_else(|| RedDBError::NotFound(name.to_string()))
662 }
663
664 pub fn fail_index(&self, name: &str) -> RedDBResult<crate::PhysicalIndexState> {
665 self.inner
666 .db
667 .fail_index(name)
668 .map_err(|err| RedDBError::Internal(err.to_string()))?
669 .ok_or_else(|| RedDBError::NotFound(name.to_string()))
670 }
671
672 pub fn mark_index_stale(&self, name: &str) -> RedDBResult<crate::PhysicalIndexState> {
673 self.inner
674 .db
675 .mark_index_stale(name)
676 .map_err(|err| RedDBError::Internal(err.to_string()))?
677 .ok_or_else(|| RedDBError::NotFound(name.to_string()))
678 }
679
680 pub fn mark_index_ready(&self, name: &str) -> RedDBResult<crate::PhysicalIndexState> {
681 self.inner
682 .db
683 .mark_index_ready(name)
684 .map_err(|err| RedDBError::Internal(err.to_string()))?
685 .ok_or_else(|| RedDBError::NotFound(name.to_string()))
686 }
687
688 pub fn warmup_index_with_lifecycle(
689 &self,
690 name: &str,
691 ) -> RedDBResult<crate::PhysicalIndexState> {
692 self.mark_index_building(name)?;
693 match self.warmup_index(name) {
694 Ok(index) => Ok(index),
695 Err(err) => {
696 let _ = self.fail_index(name);
697 Err(err)
698 }
699 }
700 }
701
702 pub fn warmup_index(&self, name: &str) -> RedDBResult<crate::PhysicalIndexState> {
703 self.inner
704 .db
705 .warmup_index(name)
706 .map_err(|err| RedDBError::Internal(err.to_string()))?
707 .ok_or_else(|| RedDBError::NotFound(name.to_string()))
708 }
709
710 pub fn rebuild_indexes(
711 &self,
712 collection: Option<&str>,
713 ) -> RedDBResult<Vec<crate::PhysicalIndexState>> {
714 self.inner
715 .db
716 .rebuild_index_registry(collection)
717 .map_err(|err| RedDBError::Internal(err.to_string()))
718 }
719
720 pub fn rebuild_indexes_with_lifecycle(
721 &self,
722 collection: Option<&str>,
723 ) -> RedDBResult<Vec<crate::PhysicalIndexState>> {
724 let target_names: Vec<String> = match collection {
725 Some(collection) => self
726 .declared_indexes_for_collection(collection)
727 .into_iter()
728 .map(|index| index.name)
729 .collect(),
730 None => self
731 .declared_indexes()
732 .into_iter()
733 .map(|index| index.name)
734 .collect(),
735 };
736
737 let mut marked_building = Vec::new();
738 for name in target_names {
739 if self.mark_index_building(&name).is_ok() {
740 marked_building.push(name);
741 }
742 }
743
744 match self.rebuild_indexes(collection) {
745 Ok(indexes) => Ok(indexes),
746 Err(err) => {
747 for name in marked_building {
748 let _ = self.fail_index(&name);
749 }
750 Err(err)
751 }
752 }
753 }
754}
755
756fn current_unix_ms_u64() -> u64 {
757 std::time::SystemTime::now()
758 .duration_since(std::time::UNIX_EPOCH)
759 .map(|d| d.as_millis() as u64)
760 .unwrap_or(0)
761}