Skip to main content

reddb_server/runtime/
impl_physical.rs

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                        // Rollup outputs keep tags inline; they do not intern
582                        // through the series dictionary.
583                        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}