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 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 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}