Skip to main content

reddb_server/runtime/
impl_primary_replica_file.rs

1use super::*;
2
3impl RedDBRuntime {
4    pub fn replica_relay_manifest_path(&self, replica_id: &str) -> Option<std::path::PathBuf> {
5        let plan = self.primary_replica_file_plan()?;
6        Some(plan.relay_manifest_path(replica_id))
7    }
8
9    pub fn replica_relay_manifest(
10        &self,
11        replica_id: &str,
12    ) -> RedDBResult<Option<reddb_file::ReplicaRelayLogManifest>> {
13        let key = format!("replication.relay.{}", hex::encode(replica_id));
14        if let Some(bytes) = crate::replication::metadata::read(&self.inner.db.store(), &key)
15            .map_err(|err| RedDBError::Internal(err.to_string()))?
16        {
17            return reddb_file::ReplicaRelayLogManifest::decode(&bytes)
18                .map(Some)
19                .map_err(|err| RedDBError::Internal(err.to_string()));
20        }
21        let Some(path) = self.replica_relay_manifest_path(replica_id) else {
22            return Ok(None);
23        };
24        match reddb_file::ReplicaRelayLogManifest::read_from_path(path) {
25            Ok(manifest) => Ok(Some(manifest)),
26            Err(reddb_file::RdbFileError::Io(error))
27                if error.kind() == std::io::ErrorKind::NotFound =>
28            {
29                Ok(None)
30            }
31            Err(error) => Err(RedDBError::Internal(error.to_string())),
32        }
33    }
34
35    pub fn primary_replica_file_plan(&self) -> Option<reddb_file::PrimaryReplicaFilePlan> {
36        self.primary_replica_file_plan_result().ok().flatten()
37    }
38
39    fn primary_replica_root(&self) -> Option<std::path::PathBuf> {
40        let data_path = self.inner.db.options().data_path.as_ref()?;
41        Some(crate::replication::primary::PrimaryReplication::primary_replica_root_for(data_path))
42    }
43
44    pub fn primary_replica_file_plan_result(
45        &self,
46    ) -> RedDBResult<Option<reddb_file::PrimaryReplicaFilePlan>> {
47        let Some(root) = self.primary_replica_root() else {
48            return Ok(None);
49        };
50        let timeline = self.primary_replica_current_timeline(&root)?;
51        Ok(Some(reddb_file::PrimaryReplicaFilePlan::new(
52            root, timeline,
53        )))
54    }
55
56    fn primary_replica_current_timeline(
57        &self,
58        root: &std::path::Path,
59    ) -> RedDBResult<reddb_file::TimelineId> {
60        let path = reddb_file::PrimaryReplicaFilePlan::new(root, reddb_file::TimelineId::initial())
61            .timeline_history_path();
62        crate::replication::metadata::timeline(&self.inner.db.store(), &path)
63            .map(|history| {
64                history
65                    .current()
66                    .unwrap_or_else(reddb_file::TimelineId::initial)
67            })
68            .map_err(|err| RedDBError::Internal(err.to_string()))
69    }
70
71    pub fn create_primary_replica_basebackup(
72        &self,
73        chunk_bytes: usize,
74    ) -> RedDBResult<Option<reddb_file::PrimaryReplicaBaseBackupManifest>> {
75        let Some(plan) = self.primary_replica_file_plan_result()? else {
76            return Ok(None);
77        };
78        self.flush()?;
79        let checkpoint_lsn = self
80            .primary_logical_head_lsn()
81            .max(self.replication_commit_lsn());
82        let snapshot = self.inner.db.store().to_binary_dump_bytes();
83        let backup = reddb_file::BaseBackupPlan::new(plan.timeline, 0, checkpoint_lsn);
84        let manifest = plan
85            .write_basebackup_snapshot_parts(backup, &snapshot, chunk_bytes)
86            .map_err(|err| RedDBError::Internal(err.to_string()))?;
87        manifest
88            .write_to_path(plan.basebackup_path(&backup))
89            .map_err(|err| RedDBError::Internal(err.to_string()))?;
90        Ok(Some(manifest))
91    }
92
93    pub fn materialize_primary_replica_basebackup_snapshot(
94        &self,
95        manifest: &reddb_file::PrimaryReplicaBaseBackupManifest,
96        parts_root: impl AsRef<std::path::Path>,
97        destination: impl AsRef<std::path::Path>,
98    ) -> RedDBResult<u64> {
99        self.materialize_primary_replica_basebackup_snapshot_for_replica(
100            manifest,
101            parts_root,
102            destination,
103            None,
104            None,
105        )
106    }
107
108    fn materialize_primary_replica_basebackup_snapshot_for_replica(
109        &self,
110        manifest: &reddb_file::PrimaryReplicaBaseBackupManifest,
111        parts_root: impl AsRef<std::path::Path>,
112        destination: impl AsRef<std::path::Path>,
113        replica_id: Option<&str>,
114        snapshot_token: Option<&str>,
115    ) -> RedDBResult<u64> {
116        let snapshot = manifest
117            .read_snapshot_parts(parts_root)
118            .map_err(|err| RedDBError::Internal(err.to_string()))?;
119        let loaded = crate::storage::unified::UnifiedStore::load_from_bytes_with_config(
120            &snapshot,
121            crate::storage::unified::UnifiedStoreConfig::default(),
122        )
123        .map_err(|err| RedDBError::Internal(format!("validate basebackup snapshot: {err}")))?;
124        loaded
125            .normalize_red_config_keys()
126            .map_err(RedDBError::from)?;
127        loaded.set_config_tree(
128            "replication",
129            &crate::json!({
130                "last_applied_lsn": manifest.checkpoint_lsn,
131                "state": "healthy",
132                "last_error": "",
133            }),
134        );
135        if let Some(replica_id) = replica_id {
136            loaded.set_config_tree(
137                "replication",
138                &crate::json!({
139                    "replica_id": replica_id,
140                }),
141            );
142        }
143        if let Some(snapshot_token) = snapshot_token {
144            loaded.set_config_tree(
145                "replication",
146                &crate::json!({
147                    "rebootstrap_snapshot_token": snapshot_token,
148                }),
149            );
150        }
151        let snapshot = loaded.to_binary_dump_bytes();
152        let destination = destination.as_ref();
153        if let Some(parent) = destination.parent() {
154            std::fs::create_dir_all(parent)?;
155        }
156        crate::storage::EmbeddedRdbArtifact::create_with_snapshot(destination, &snapshot)?;
157        Ok(manifest.checkpoint_lsn)
158    }
159
160    pub fn replica_rebootstrap_staging_root(&self) -> Option<std::path::PathBuf> {
161        let data_path = self.inner.db.options().data_path.as_ref()?;
162        Some(reddb_file::layout::rebootstrap_staging_root(data_path))
163    }
164
165    pub fn replica_rebootstrap_pending_path(&self) -> Option<std::path::PathBuf> {
166        let data_path = self.inner.db.options().data_path.as_ref()?;
167        Some(reddb_file::layout::rebootstrap_pending_path(data_path))
168    }
169
170    pub(crate) async fn stage_primary_replica_rebootstrap_from_snapshot(
171        &self,
172        client: &mut crate::grpc::proto::red_db_client::RedDbClient<tonic::transport::Channel>,
173        chunk_bytes: usize,
174    ) -> RedDBResult<Option<u64>> {
175        let Some(parts_root) = self.replica_rebootstrap_staging_root() else {
176            return Ok(None);
177        };
178        let Some(pending_path) = self.replica_rebootstrap_pending_path() else {
179            return Ok(None);
180        };
181        let data_path = self
182            .inner
183            .db
184            .options()
185            .data_path
186            .as_ref()
187            .ok_or_else(|| RedDBError::Internal("replica data path unavailable".into()))?;
188        let intent_log_path = reddb_file::layout::rebootstrap_intent_log_path(data_path);
189        let intent_log = crate::telemetry::admin_intent_log::AdminIntentLog::open(intent_log_path)
190            .map_err(|err| RedDBError::Internal(err.to_string()))?;
191        let replica_id = self.resolve_replica_id();
192        let bootstrapper =
193            crate::replication::replica::ReplicaBootstrapper::new(replica_id.clone());
194        let source_lsn = self.config_u64("replication.last_applied_lsn", 0);
195        let _ = std::fs::remove_file(&pending_path);
196
197        let chunk_bytes = chunk_bytes.max(1);
198        let (resume, mut bootstrap_handle) = match bootstrapper.resume(&intent_log) {
199            Some((resume, handle)) => (Some(resume), handle),
200            None => (
201                None,
202                bootstrapper
203                    .begin(&intent_log, source_lsn, 0)
204                    .map_err(|err| RedDBError::Internal(err.to_string()))?,
205            ),
206        };
207        let mut token: Option<String> = resume
208            .as_ref()
209            .and_then(|resume| resume.snapshot_token.clone());
210        let mut manifest: Option<reddb_file::PrimaryReplicaBaseBackupManifest> = None;
211        let mut written = std::collections::BTreeSet::new();
212        let mut offset = resume
213            .as_ref()
214            .map(|resume| resume.snapshot_offset)
215            .unwrap_or(0);
216
217        let request_auth = crate::replication::request_auth::ReplicaRequestAuth::from_env()
218            .map_err(|message| RedDBError::InvalidOperation(message.into()))?;
219        loop {
220            let mut request = tonic::Request::new(crate::grpc::proto::Empty {});
221            request_auth.authorize(&mut request);
222            request.metadata_mut().insert(
223                "x-reddb-replica-id",
224                replica_id
225                    .parse()
226                    .map_err(|err| RedDBError::Internal(format!("replica id metadata: {err}")))?,
227            );
228            request.metadata_mut().insert(
229                "x-reddb-snapshot-max-bytes",
230                chunk_bytes
231                    .to_string()
232                    .parse()
233                    .map_err(|err| RedDBError::Internal(format!("snapshot max bytes: {err}")))?,
234            );
235            request.metadata_mut().insert(
236                "x-reddb-snapshot-offset",
237                offset
238                    .to_string()
239                    .parse()
240                    .map_err(|err| RedDBError::Internal(format!("snapshot offset: {err}")))?,
241            );
242            if let Some(token) = &token {
243                request.metadata_mut().insert(
244                    "x-reddb-snapshot-token",
245                    token.parse().map_err(|err| {
246                        RedDBError::Internal(format!("snapshot token metadata: {err}"))
247                    })?,
248                );
249            }
250
251            let response = client
252                .replication_snapshot(request)
253                .await
254                .map_err(|err| RedDBError::Internal(format!("replication snapshot: {err}")))?;
255            let payload = reddb_wire::replication::BaseBackupChunk::decode_json(
256                response.into_inner().payload.as_bytes(),
257            )
258            .map_err(|err| RedDBError::Internal(format!("parse replication snapshot: {err}")))?;
259            if token.is_none() {
260                token = payload.snapshot_token.clone();
261            }
262            let staged =
263                crate::replication::replica::stage_basebackup_snapshot_chunk(&payload, &parts_root)
264                    .map_err(|err| RedDBError::Internal(err.to_string()))?
265                    .ok_or_else(|| {
266                        RedDBError::Internal(
267                            "replication snapshot did not include basebackup payload".into(),
268                        )
269                    })?;
270            if let Some(existing) = &manifest {
271                if existing != &staged.manifest {
272                    return Err(RedDBError::Internal(
273                        "replication snapshot basebackup manifest changed while downloading".into(),
274                    ));
275                }
276            } else {
277                manifest = Some(staged.manifest.clone());
278                written.extend(
279                    crate::replication::replica::recover_staged_basebackup_chunks(
280                        &staged.manifest,
281                        &parts_root,
282                    )
283                    .map_err(|err| RedDBError::Internal(err.to_string()))?,
284                );
285            }
286            if let Some(ordinal) = staged.chunk_ordinal {
287                written.insert(ordinal);
288            }
289            let current = manifest
290                .as_ref()
291                .expect("manifest set after staging basebackup chunk");
292            if current
293                .chunks
294                .iter()
295                .all(|chunk| written.contains(&chunk.ordinal))
296            {
297                current
298                    .verify_snapshot_parts(&parts_root)
299                    .map_err(|err| RedDBError::Internal(err.to_string()))?;
300                let snapshot_token = token.as_deref().ok_or_else(|| {
301                    RedDBError::Internal(
302                        "replication snapshot omitted its installation token".into(),
303                    )
304                })?;
305                let checkpoint_lsn = self
306                    .materialize_primary_replica_basebackup_snapshot_for_replica(
307                        current,
308                        &parts_root,
309                        &pending_path,
310                        Some(&replica_id),
311                        Some(snapshot_token),
312                    )?;
313                let data_path =
314                    self.inner.db.options().data_path.as_ref().ok_or_else(|| {
315                        RedDBError::Internal("replica data path unavailable".into())
316                    })?;
317                reddb_file::write_rebootstrap_ready_marker(
318                    data_path,
319                    &reddb_file::ReplicaRebootstrapReadyMarker {
320                        pending_path: pending_path.clone(),
321                        checkpoint_lsn,
322                        timeline: current.timeline,
323                    },
324                )
325                .map_err(|err| RedDBError::Internal(err.to_string()))?;
326                self.inner.db.store().set_config_tree(
327                    "replication",
328                    &crate::json!({
329                        "state": "rebootstrap_ready",
330                        "rebootstrap_pending_path": pending_path.display().to_string(),
331                        "rebootstrap_checkpoint_lsn": checkpoint_lsn,
332                        "rebootstrap_timeline": current.timeline.0,
333                    }),
334                );
335                bootstrap_handle
336                    .complete(current.chunks.len() as u64, 0)
337                    .map_err(|err| RedDBError::Internal(err.to_string()))?;
338                return Ok(Some(checkpoint_lsn));
339            }
340            let next = current
341                .chunks
342                .iter()
343                .find(|chunk| !written.contains(&chunk.ordinal))
344                .ok_or_else(|| RedDBError::Internal("basebackup chunk tracking stalled".into()))?;
345            offset = next.snapshot_offset;
346            if let Some(snapshot_token) = token.as_deref() {
347                bootstrap_handle
348                    .checkpoint_snapshot_transfer(
349                        snapshot_token,
350                        offset,
351                        source_lsn,
352                        written.len() as u64,
353                    )
354                    .map_err(|err| RedDBError::Internal(err.to_string()))?;
355            }
356        }
357    }
358
359    pub fn primary_replica_slot_catalog(
360        &self,
361    ) -> RedDBResult<Option<reddb_file::ReplicationSlotCatalog>> {
362        let store = self.inner.db.store();
363        let _publication = store.vault_publication_lock.lock();
364        if let Some(primary) = self.inner.db.replication.as_ref() {
365            primary
366                .ensure_slot_metadata_durable()
367                .map_err(|err| RedDBError::Internal(err.to_string()))?;
368        }
369        if let Some(bytes) = crate::replication::metadata::read(
370            &self.inner.db.store(),
371            crate::replication::metadata::SLOTS,
372        )
373        .map_err(|err| RedDBError::Internal(err.to_string()))?
374        {
375            return reddb_file::ReplicationSlotCatalog::decode(&bytes)
376                .map(Some)
377                .map_err(|err| RedDBError::Internal(err.to_string()));
378        }
379        let Some(plan) = self.primary_replica_file_plan_result()? else {
380            return Ok(None);
381        };
382        match reddb_file::ReplicationSlotCatalog::read_from_path(plan.slots_path()) {
383            Ok(catalog) => Ok(Some(catalog)),
384            Err(reddb_file::RdbFileError::Io(err))
385                if err.kind() == std::io::ErrorKind::NotFound =>
386            {
387                Ok(None)
388            }
389            Err(err) => Err(RedDBError::Internal(err.to_string())),
390        }
391    }
392
393    fn primary_replica_fork_lsns(&self) -> RedDBResult<Vec<u64>> {
394        let Some(data_path) = self.inner.db.options().data_path.as_ref() else {
395            return Ok(Vec::new());
396        };
397        self.inner
398            .db
399            .store()
400            .operational_manifest_for_path(data_path)
401            .list_forks()
402            .map(|forks| forks.into_iter().map(|fork| fork.fork_lsn).collect())
403            .map_err(|err| RedDBError::Internal(err.to_string()))
404    }
405
406    pub fn primary_replica_wal_retention_plan(
407        &self,
408    ) -> RedDBResult<Option<reddb_file::WalRetentionPlan>> {
409        let Some(plan) = self.primary_replica_file_plan_result()? else {
410            return Ok(None);
411        };
412        let Some(catalog) = self.primary_replica_slot_catalog()? else {
413            return Ok(None);
414        };
415        let current_lsn = self
416            .primary_logical_head_lsn()
417            .max(self.replication_commit_lsn());
418        let fork_lsns = self.primary_replica_fork_lsns()?;
419        Ok(Some(
420            plan.plan_wal_retention_with_fork_lsns(&catalog, &fork_lsns, current_lsn)
421                .map_err(|err| RedDBError::Internal(err.to_string()))?,
422        ))
423    }
424
425    pub fn primary_replica_catchup_mode(
426        &self,
427        available_from_lsn: u64,
428        replica_lsn: u64,
429    ) -> RedDBResult<Option<reddb_file::ReplicaCatchupMode>> {
430        let Some(plan) = self.primary_replica_file_plan_result()? else {
431            return Ok(None);
432        };
433        let basebackups = plan
434            .list_basebackups()
435            .map_err(|err| RedDBError::Internal(err.to_string()))?;
436        Ok(Some(plan.catchup_mode_with_basebackups(
437            available_from_lsn,
438            replica_lsn,
439            &basebackups,
440        )))
441    }
442
443    pub fn primary_replica_timeline_history_path(&self) -> Option<std::path::PathBuf> {
444        let root = self.primary_replica_root()?;
445        Some(
446            reddb_file::PrimaryReplicaFilePlan::new(root, reddb_file::TimelineId::initial())
447                .timeline_history_path(),
448        )
449    }
450
451    /// Authoritative history in the database, with read-only legacy import fallback.
452    pub fn primary_replica_timeline_history(
453        &self,
454    ) -> RedDBResult<Option<reddb_file::TimelineHistory>> {
455        let Some(path) = self.primary_replica_timeline_history_path() else {
456            return Ok(None);
457        };
458        crate::replication::metadata::timeline(&self.inner.db.store(), &path)
459            .map(Some)
460            .map_err(|err| RedDBError::Internal(err.to_string()))
461    }
462
463    pub fn primary_replica_rejoin_decision(
464        &self,
465        node_timeline: reddb_file::TimelineId,
466        node_flushed_lsn: u64,
467        available_from_lsn: u64,
468    ) -> RedDBResult<Option<reddb_file::RejoinDecision>> {
469        let Some(history) = self.primary_replica_timeline_history()? else {
470            return Ok(None);
471        };
472        Ok(Some(history.rejoin_decision(
473            node_timeline,
474            node_flushed_lsn,
475            available_from_lsn,
476        )))
477    }
478
479    pub fn persist_primary_replica_rejoin_plan(
480        &self,
481        node_timeline: reddb_file::TimelineId,
482        node_flushed_lsn: u64,
483        available_from_lsn: u64,
484    ) -> RedDBResult<Option<reddb_file::RejoinDecision>> {
485        let Some(decision) = self.primary_replica_rejoin_decision(
486            node_timeline,
487            node_flushed_lsn,
488            available_from_lsn,
489        )?
490        else {
491            return Ok(None);
492        };
493
494        let (state, target_timeline, rewind_to_lsn, start_lsn) = match decision {
495            reddb_file::RejoinDecision::AlreadyCurrent => {
496                ("timeline_current", node_timeline.0, 0, node_flushed_lsn)
497            }
498            reddb_file::RejoinDecision::FollowNewTimeline {
499                target_timeline,
500                start_lsn,
501            } => ("rejoin_follow_wal", target_timeline.0, 0, start_lsn),
502            reddb_file::RejoinDecision::Rewind {
503                target_timeline,
504                rewind_to_lsn,
505            } => (
506                "rejoin_rewind_required",
507                target_timeline.0,
508                rewind_to_lsn,
509                0,
510            ),
511            reddb_file::RejoinDecision::Reclone => ("reclone_required", 0, 0, 0),
512        };
513        self.inner.db.store().set_config_tree(
514            "replication",
515            &crate::json!({
516                "state": state,
517                "rejoin_node_timeline": node_timeline.0,
518                "rejoin_node_flushed_lsn": node_flushed_lsn,
519                "rejoin_available_from_lsn": available_from_lsn,
520                "rejoin_target_timeline": target_timeline,
521                "rejoin_rewind_to_lsn": rewind_to_lsn,
522                "rejoin_start_lsn": start_lsn,
523                "rejoin_rewind_confirmed_timeline": 0,
524                "rejoin_rewind_confirmed_lsn": 0,
525            }),
526        );
527
528        Ok(Some(decision))
529    }
530
531    pub fn prune_primary_replica_wal_segments(
532        &self,
533    ) -> RedDBResult<Option<reddb_file::WalPruneResult>> {
534        let current_lsn = self
535            .primary_logical_head_lsn()
536            .max(self.replication_commit_lsn());
537        self.prune_primary_replica_wal_segments_at(current_lsn)
538    }
539
540    fn prune_primary_replica_wal_segments_at(
541        &self,
542        current_lsn: u64,
543    ) -> RedDBResult<Option<reddb_file::WalPruneResult>> {
544        let Some(plan) = self.primary_replica_file_plan_result()? else {
545            return Ok(None);
546        };
547        let Some(catalog) = self.primary_replica_slot_catalog()? else {
548            return Ok(None);
549        };
550        let fork_lsns = self.primary_replica_fork_lsns()?;
551        Ok(Some(
552            plan.prune_wal_segments_with_fork_lsns(&catalog, &fork_lsns, current_lsn)
553                .map_err(|err| RedDBError::Internal(err.to_string()))?,
554        ))
555    }
556
557    pub fn ack_primary_replica_lsn_and_prune(
558        &self,
559        replica_id: &str,
560        applied_lsn: u64,
561        durable_lsn: u64,
562        apply_errors_total: u64,
563        divergence_total: u64,
564    ) -> RedDBResult<Option<reddb_file::WalPruneResult>> {
565        let Some(repl) = self.inner.db.replication.as_ref() else {
566            return Ok(None);
567        };
568        repl.ack_replica_lsn_with_observability(
569            replica_id,
570            applied_lsn,
571            durable_lsn,
572            apply_errors_total,
573            divergence_total,
574        );
575        self.refresh_replication_flow_control();
576        let current_lsn = self
577            .primary_logical_head_lsn()
578            .max(self.replication_commit_lsn())
579            .max(applied_lsn)
580            .max(durable_lsn);
581        self.prune_primary_replica_wal_segments_at(current_lsn)
582    }
583
584    pub fn record_failover_timeline_promotion(
585        &self,
586        replica_id: &str,
587        applied_lsn: u64,
588    ) -> RedDBResult<reddb_file::TimelineHistory> {
589        let store = self.inner.db.store();
590        let _publication = store.vault_publication_lock.lock();
591        let Some(history) = self.primary_replica_timeline_history()? else {
592            return Ok(reddb_file::TimelineHistory::new(
593                crate::utils::now_unix_millis(),
594            ));
595        };
596        let now_ms = crate::utils::now_unix_millis();
597        let parent = history
598            .current()
599            .unwrap_or_else(reddb_file::TimelineId::initial);
600        let candidate = reddb_file::PromotionCandidate {
601            replica_id: replica_id.to_string(),
602            timeline: parent,
603            received_lsn: applied_lsn,
604            flushed_lsn: applied_lsn,
605            applied_lsn,
606        };
607        let promoted = history
608            .promotion_history(&candidate, parent.next(), now_ms)
609            .map_err(|err| RedDBError::Internal(err.to_string()))?;
610        if let Some(mut catalog) = self.primary_replica_slot_catalog()? {
611            if !history
612                .entries
613                .iter()
614                .any(|entry| entry.timeline == catalog.timeline)
615            {
616                return Err(RedDBError::Internal(format!(
617                    "slot catalog timeline {} is outside the promotion history",
618                    catalog.timeline.0
619                )));
620            }
621            let previous_count = catalog.slots.len();
622            catalog.slots.retain(|slot| slot.replica_id != replica_id);
623            if catalog.slots.len() != previous_count {
624                // The candidate is becoming the writer, not a downstream
625                // consumer of itself. Retire its old slot durably before
626                // publishing the timeline so a crash cannot resurrect the pin.
627                crate::replication::metadata::write(
628                    &store,
629                    crate::replication::metadata::SLOTS,
630                    &catalog
631                        .encode()
632                        .map_err(|err| RedDBError::Internal(err.to_string()))?,
633                )
634                .map_err(|err| RedDBError::Internal(err.to_string()))?;
635            }
636        }
637        crate::replication::metadata::write(
638            &self.inner.db.store(),
639            crate::replication::metadata::TIMELINE,
640            &promoted.encode(),
641        )
642        .map_err(|err| RedDBError::Internal(err.to_string()))?;
643        if let Some(path) = self.primary_replica_timeline_history_path() {
644            crate::replication::metadata::retire_legacy(&path)
645                .map_err(|err| RedDBError::Internal(err.to_string()))?;
646        }
647        Ok(promoted)
648    }
649
650    pub fn record_replica_relay_batch<T: AsRef<[u8]>>(
651        &self,
652        replica_id: &str,
653        records: &[(u64, T)],
654        applied_lsn: u64,
655    ) -> RedDBResult<()> {
656        let store = self.inner.db.store();
657        let _publication = store.vault_publication_lock.lock();
658        let Some(plan) = self.primary_replica_file_plan_result()? else {
659            return Ok(());
660        };
661        let path = plan.relay_manifest_path(replica_id);
662        let Some((first_lsn, _)) = records.first() else {
663            return Ok(());
664        };
665        let end_lsn = records
666            .iter()
667            .map(|(lsn, _)| *lsn)
668            .max()
669            .unwrap_or(*first_lsn);
670        let mut manifest = match self.replica_relay_manifest(replica_id)? {
671            Some(manifest) => {
672                let relay_dir = path.parent().ok_or_else(|| {
673                    RedDBError::Internal("relay manifest path has no parent".into())
674                })?;
675                manifest
676                    .validate_segments(relay_dir)
677                    .map_err(|err| RedDBError::Internal(err.to_string()))?;
678                manifest
679            }
680            None => reddb_file::ReplicaRelayLogManifest::new(
681                replica_id,
682                reddb_file::TimelineId::initial(),
683            ),
684        };
685        if manifest.replica_id != replica_id {
686            return Err(RedDBError::Internal(format!(
687                "relay manifest replica_id {} does not match {}",
688                manifest.replica_id, replica_id
689            )));
690        }
691        if manifest.timeline != reddb_file::TimelineId::initial() {
692            return Err(RedDBError::Internal(format!(
693                "relay manifest timeline {} is not current timeline {}",
694                manifest.timeline.0,
695                reddb_file::TimelineId::initial().0
696            )));
697        }
698
699        if end_lsn > manifest.flushed_lsn {
700            let new_records = records
701                .iter()
702                .filter(|(lsn, _)| *lsn > manifest.flushed_lsn)
703                .map(|(lsn, payload)| {
704                    reddb_file::ReplicaRelayLogRecord::new(*lsn, payload.as_ref().to_vec())
705                })
706                .collect::<Vec<_>>();
707            let segment = reddb_file::ReplicaRelayLogSegment::from_records(
708                reddb_file::TimelineId::initial(),
709                new_records,
710            )
711            .map_err(|err| RedDBError::Internal(err.to_string()))?;
712            let start_lsn = segment.start_lsn;
713            let end_lsn = segment.end_lsn;
714            let relative_path = reddb_file::layout::relay_segment_relative_path(start_lsn, end_lsn);
715            let segment_path = path
716                .parent()
717                .ok_or_else(|| RedDBError::Internal("relay manifest path has no parent".into()))?
718                .join(&relative_path);
719            segment
720                .write_to_path(&segment_path)
721                .map_err(|err| RedDBError::Internal(err.to_string()))?;
722            manifest
723                .push_segment(
724                    reddb_file::RelayLogSegmentRef::new(
725                        relative_path,
726                        start_lsn,
727                        end_lsn,
728                        segment
729                            .checksum()
730                            .map_err(|err| RedDBError::Internal(err.to_string()))?,
731                    )
732                    .map_err(|err| RedDBError::Internal(err.to_string()))?,
733                )
734                .map_err(|err| RedDBError::Internal(err.to_string()))?;
735        }
736        manifest
737            .mark_applied(applied_lsn.min(manifest.flushed_lsn))
738            .map_err(|err| RedDBError::Internal(err.to_string()))?;
739        let bytes = manifest
740            .encode()
741            .map_err(|err| RedDBError::Internal(err.to_string()))?;
742        crate::replication::metadata::write(
743            &self.inner.db.store(),
744            &format!("replication.relay.{}", hex::encode(replica_id)),
745            &bytes,
746        )
747        .map_err(|err| RedDBError::Internal(err.to_string()))?;
748        crate::replication::metadata::retire_legacy(&path)
749            .map_err(|err| RedDBError::Internal(err.to_string()))
750    }
751}