1use super::*;
2
3impl RedDBRuntime {
4 pub fn primary_replica_snapshots(&self) -> Vec<crate::replication::primary::ReplicaState> {
8 self.inner
9 .db
10 .replication
11 .as_ref()
12 .map(|repl| repl.replica_snapshots())
13 .unwrap_or_default()
14 }
15
16 pub fn primary_logical_head_lsn(&self) -> u64 {
20 self.inner
21 .db
22 .replication
23 .as_ref()
24 .map(|repl| repl.current_logical_lsn())
25 .unwrap_or(0)
26 }
27
28 pub fn replication_full_resync_count(&self) -> u64 {
32 self.inner
33 .db
34 .replication
35 .as_ref()
36 .map(|repl| repl.full_resync_count())
37 .unwrap_or(0)
38 }
39
40 pub fn replication_partial_resync_count(&self) -> u64 {
43 self.inner
44 .db
45 .replication
46 .as_ref()
47 .map(|repl| repl.partial_resync_count())
48 .unwrap_or(0)
49 }
50
51 pub fn replication_reconnects_count(&self) -> u64 {
59 self.inner.replica_link_metrics.reconnects_total()
60 }
61
62 pub fn replication_replica_id(&self) -> String {
68 self.config_string("red.replication.replica_id", "")
69 }
70
71 pub fn observe_replica_link_state(&self, state: &str) {
77 self.inner.replica_link_metrics.observe_state(state);
78 }
79
80 pub fn enforce_primary_replica_retention_limits(
81 &self,
82 ) -> Vec<(String, reddb_file::ReplicationSlotInvalidationCause)> {
83 self.inner
84 .db
85 .replication
86 .as_ref()
87 .map(|repl| repl.enforce_retention_limits(crate::utils::now_unix_millis() as u128))
88 .unwrap_or_default()
89 }
90
91 pub fn node_id(&self) -> String {
96 self.resolve_replica_id()
97 }
98
99 pub fn refresh_replication_flow_control(&self) -> bool {
110 let flow = self.inner.write_gate.flow_control();
111 if !flow.is_enabled() {
112 return false;
113 }
114 let Some(repl) = self.inner.db.replication.as_ref() else {
115 return false;
116 };
117 let primary_lsn = repl.current_logical_lsn();
118 let replicas = repl.replica_snapshots();
119 flow.observe(&replicas, primary_lsn)
120 }
121
122 pub fn commit_policy(&self) -> crate::replication::CommitPolicy {
127 crate::replication::CommitPolicy::from_env()
128 }
129
130 pub fn resolve_commit_policy(
142 &self,
143 model: crate::cluster::CollectionDataModel,
144 collection_override: Option<crate::replication::CommitPolicy>,
145 ) -> Result<crate::cluster::CommitPolicyResolution, crate::cluster::CommitPolicyViolation> {
146 crate::cluster::resolve_commit_policy(
147 self.commit_policy(),
148 collection_override,
149 model,
150 crate::cluster::HaIntent::from_env(),
151 )
152 }
153
154 pub fn resolve_request_commit_policy(
155 &self,
156 request_override: Option<crate::replication::CommitPolicy>,
157 ) -> Result<crate::cluster::CommitPolicyResolution, crate::cluster::CommitPolicyViolation> {
158 let floor = crate::cluster::CommitPolicyResolution {
159 effective: self.commit_policy(),
160 source: crate::cluster::ResolutionSource::ClusterDefault,
161 guardrail: crate::cluster::GuardrailDisposition::NotApplicable,
162 };
163 crate::cluster::resolve_request_commit_policy(floor, request_override)
164 }
165
166 pub fn primary_replica_durability(&self) -> reddb_file::ReplicationDurability {
167 Self::primary_replica_durability_for_policy(self.commit_policy())
168 }
169
170 pub(crate) fn primary_replica_durability_for_policy(
171 policy: crate::replication::CommitPolicy,
172 ) -> reddb_file::ReplicationDurability {
173 match policy {
174 crate::replication::CommitPolicy::AckN(n) if n > 0 => {
175 reddb_file::ReplicationDurability::RemoteFlush {
176 quorum: u16::try_from(n).unwrap_or(u16::MAX),
177 }
178 }
179 _ => reddb_file::ReplicationDurability::Async,
180 }
181 }
182
183 pub fn replica_apply_error_counts(
189 &self,
190 ) -> [(crate::replication::logical::ApplyErrorKind, u64); 6] {
191 self.inner.replica_apply_metrics.snapshot()
192 }
193
194 pub fn replica_apply_throughput_counts(&self) -> (u64, u64) {
200 self.inner.replica_apply_metrics.snapshot_throughput()
201 }
202
203 pub fn commit_waiter_snapshot(&self) -> Vec<(String, u64)> {
207 self.inner
208 .db
209 .replication
210 .as_ref()
211 .map(|repl| repl.commit_waiter.snapshot())
212 .unwrap_or_default()
213 }
214
215 pub fn commit_waiter_metrics_snapshot(&self) -> (u64, u64, u64, u64) {
218 self.inner
219 .db
220 .replication
221 .as_ref()
222 .map(|repl| repl.commit_waiter.metrics_snapshot())
223 .unwrap_or((0, 0, 0, 0))
224 }
225
226 pub fn commit_watermark(&self) -> u64 {
230 match self.primary_replica_durability() {
231 reddb_file::ReplicationDurability::RemoteWrite { quorum }
232 | reddb_file::ReplicationDurability::RemoteFlush { quorum }
233 | reddb_file::ReplicationDurability::RemoteApply { quorum }
234 if quorum > 0 =>
235 {
236 self.inner
237 .db
238 .replication
239 .as_ref()
240 .map(|repl| repl.commit_waiter.commit_watermark(u32::from(quorum)))
241 .unwrap_or(0)
242 }
243 _ if matches!(
244 self.commit_policy(),
245 crate::replication::CommitPolicy::Quorum
246 ) =>
247 {
248 self.inner
249 .db
250 .quorum
251 .as_ref()
252 .map(|q| q.commit_watermark())
253 .unwrap_or(0)
254 }
255 _ => 0,
256 }
257 }
258
259 pub fn await_replica_acks(
268 &self,
269 target_lsn: u64,
270 count: u32,
271 timeout: std::time::Duration,
272 ) -> crate::replication::AwaitOutcome {
273 match &self.inner.db.replication {
274 Some(repl) => repl.commit_waiter.await_acks(target_lsn, count, timeout),
275 None => {
276 crate::replication::AwaitOutcome::NotRequired
280 }
281 }
282 }
283
284 pub fn enforce_commit_policy(
288 &self,
289 post_lsn: u64,
290 ) -> RedDBResult<crate::replication::AwaitOutcome> {
291 let policy = crate::replication::CommitPolicy::from_env_strict()
292 .map_err(RedDBError::InvalidConfig)?;
293 self.enforce_commit_policy_for_policy(post_lsn, policy)
294 }
295
296 pub fn enforce_commit_policy_for_request(
297 &self,
298 post_lsn: u64,
299 request_override: Option<crate::replication::CommitPolicy>,
300 ) -> RedDBResult<crate::replication::AwaitOutcome> {
301 let policy = self.validated_request_commit_policy(request_override)?;
302 self.enforce_commit_policy_for_policy(post_lsn, policy)
303 }
304
305 pub(super) fn enforce_commit_policy_for_policy(
306 &self,
307 post_lsn: u64,
308 policy: crate::replication::CommitPolicy,
309 ) -> RedDBResult<crate::replication::AwaitOutcome> {
310 self.validate_commit_policy(policy)?;
311 if post_lsn == 0 {
312 return Ok(crate::replication::AwaitOutcome::NotRequired);
313 }
314 if matches!(policy, crate::replication::CommitPolicy::Quorum) {
315 return match self.inner.db.wait_for_replication_quorum(post_lsn) {
316 Ok(()) => Ok(crate::replication::AwaitOutcome::Reached(0)),
317 Err(err) => {
318 tracing::warn!(
319 target: "reddb::commit",
320 post_lsn,
321 error = %err,
322 "quorum: timed out waiting for commit watermark"
323 );
324 Err(RedDBError::CommitUncertain {
325 lsn: post_lsn,
326 detail: err.to_string(),
327 })
328 }
329 };
330 }
331
332 let n = match policy {
333 crate::replication::CommitPolicy::AckN(n) if n > 0 => n,
334 _ => return Ok(crate::replication::AwaitOutcome::NotRequired),
335 };
336 let timeout_ms = std::env::var("RED_REPLICATION_ACK_TIMEOUT_MS")
337 .ok()
338 .and_then(|v| v.parse::<u64>().ok())
339 .unwrap_or(5_000);
340 let outcome =
341 self.await_replica_acks(post_lsn, n, std::time::Duration::from_millis(timeout_ms));
342 {
343 use crate::runtime::control_events::{EventKind, Outcome, Sensitivity};
344 let (event_outcome, fields) = match &outcome {
345 crate::replication::AwaitOutcome::Reached(count) => (
346 Outcome::Allowed,
347 vec![
348 (
349 "post_lsn".to_string(),
350 Sensitivity::raw(post_lsn.to_string()),
351 ),
352 ("required".to_string(), Sensitivity::raw(n.to_string())),
353 ("observed".to_string(), Sensitivity::raw(count.to_string())),
354 (
355 "timeout_ms".to_string(),
356 Sensitivity::raw(timeout_ms.to_string()),
357 ),
358 ],
359 ),
360 crate::replication::AwaitOutcome::TimedOut { observed, required } => (
361 Outcome::Error,
362 vec![
363 (
364 "post_lsn".to_string(),
365 Sensitivity::raw(post_lsn.to_string()),
366 ),
367 (
368 "required".to_string(),
369 Sensitivity::raw(required.to_string()),
370 ),
371 (
372 "observed".to_string(),
373 Sensitivity::raw(observed.to_string()),
374 ),
375 (
376 "timeout_ms".to_string(),
377 Sensitivity::raw(timeout_ms.to_string()),
378 ),
379 ],
380 ),
381 crate::replication::AwaitOutcome::NotRequired => (Outcome::Allowed, Vec::new()),
382 };
383 if !fields.is_empty() {
384 self.emit_control_event(
385 EventKind::ReplicationSafety,
386 event_outcome,
387 "replication_commit_policy",
388 Some(format!("replication:lsn:{post_lsn}")),
389 None,
390 fields,
391 ).map_err(|error| {
392 if matches!(outcome, crate::replication::AwaitOutcome::TimedOut { .. }) {
393 RedDBError::CommitUncertain {
394 lsn: post_lsn,
395 detail: format!("replica ACK threshold missed; recording the outcome also failed: {error}"),
396 }
397 } else { error }
398 })?;
399 }
400 }
401 if let crate::replication::AwaitOutcome::TimedOut { observed, required } = &outcome {
402 tracing::warn!(
403 target: "reddb::commit",
404 post_lsn,
405 observed = *observed,
406 required = *required,
407 timeout_ms,
408 "ack_n: timed out waiting for replicas"
409 );
410 return Err(RedDBError::CommitUncertain {
411 lsn: post_lsn,
412 detail: format!("observed={observed} required={required}"),
413 });
414 }
415 Ok(outcome)
416 }
417
418 pub(crate) fn validate_commit_policy(
419 &self,
420 policy: crate::replication::CommitPolicy,
421 ) -> RedDBResult<()> {
422 use crate::replication::CommitPolicy;
423 match policy {
424 CommitPolicy::Local | CommitPolicy::AckN(0) => Ok(()),
425 CommitPolicy::RemoteWal => Err(RedDBError::InvalidConfig(
426 "remote_wal commit acknowledgement is not implemented".into(),
427 )),
428 CommitPolicy::AckN(n) if n > u32::from(u16::MAX) => Err(RedDBError::InvalidConfig(
429 "ack_n exceeds the supported replica quorum size".into(),
430 )),
431 _ if self.inner.db.replication.is_none() => Err(RedDBError::InvalidConfig(
432 "synchronous commit policy requires a replication primary".into(),
433 )),
434 CommitPolicy::Quorum if self.inner.db.options().replication.quorum.is_async() => {
435 Err(RedDBError::InvalidConfig(
436 "quorum commit policy requires a synchronous quorum configuration".into(),
437 ))
438 }
439 _ => Ok(()),
440 }
441 }
442
443 pub(crate) fn validated_request_commit_policy(
444 &self,
445 request: Option<crate::replication::CommitPolicy>,
446 ) -> RedDBResult<crate::replication::CommitPolicy> {
447 let configured = crate::replication::CommitPolicy::from_env_strict()
448 .map_err(RedDBError::InvalidConfig)?;
449 if let Some(request) = request {
450 reject_incomparable_commit_policies(configured, request)?;
451 }
452 let floor = crate::cluster::CommitPolicyResolution {
453 effective: configured,
454 source: crate::cluster::ResolutionSource::ClusterDefault,
455 guardrail: crate::cluster::GuardrailDisposition::NotApplicable,
456 };
457 let effective = crate::cluster::resolve_request_commit_policy(floor, request)
458 .map_err(commit_policy_violation_error)?
459 .effective;
460 self.validate_commit_policy(effective)?;
461 Ok(effective)
462 }
463}
464
465pub(crate) fn commit_policy_violation_error(
466 violation: crate::cluster::CommitPolicyViolation,
467) -> RedDBError {
468 let code = violation.code();
469 let detail = violation.message();
470 let mut validation = crate::json::Map::new();
471 validation.insert(
472 "code".to_string(),
473 crate::json::Value::String(code.to_string()),
474 );
475 validation.insert(
476 "message".to_string(),
477 crate::json::Value::String(detail.clone()),
478 );
479 RedDBError::Validation {
480 message: format!("{code}: {detail}"),
481 validation: crate::json::Value::Object(validation),
482 }
483}
484
485pub(super) fn reject_incomparable_commit_policies(
488 prior: crate::replication::CommitPolicy,
489 next: crate::replication::CommitPolicy,
490) -> RedDBResult<()> {
491 use crate::replication::CommitPolicy;
492 if matches!((prior, next), (CommitPolicy::AckN(n), CommitPolicy::Quorum) | (CommitPolicy::Quorum, CommitPolicy::AckN(n)) if n > 0)
493 {
494 return Err(RedDBError::InvalidConfig(
495 "cannot mix AckN and Quorum commit requirements".into(),
496 ));
497 }
498 Ok(())
499}