Run one catch-up cycle: read new WAL records, dispatch timeseries batches. Uses paginated mmap replay to bound memory. Passes WAL LSNs to the Data Plane for deduplication (records already ingested are skipped).
(shared: &SharedState)
| 84 | /// Uses paginated mmap replay to bound memory. Passes WAL LSNs to the |
| 85 | /// Data Plane for deduplication (records already ingested are skipped). |
| 86 | async fn run_catchup_cycle(shared: &SharedState) -> CatchupResult { |
| 87 | // Backpressure gate: don't compete with live ingest for SPSC slots. |
| 88 | if shared.max_spsc_utilization() > 50 { |
| 89 | return CatchupResult::Backpressured; |
| 90 | } |
| 91 | |
| 92 | let catchup_lsn = shared.wal_catchup_lsn.load(Ordering::Acquire); |
| 93 | |
| 94 | // Read at most PAGE_SIZE WAL records via sequential I/O (bounded memory). |
| 95 | // Uses sequential I/O (not mmap) to safely read the active segment |
| 96 | // even when written via O_DIRECT which bypasses the page cache. |
| 97 | let (records, has_more) = match shared |
| 98 | .wal |
| 99 | .replay_from_limit(Lsn::new(catchup_lsn + 1), PAGE_SIZE) |
| 100 | { |
| 101 | Ok(r) => r, |
| 102 | Err(e) => { |
| 103 | debug!(error = %e, lsn = catchup_lsn, "WAL catch-up replay failed"); |
| 104 | return CatchupResult::Idle; |
| 105 | } |
| 106 | }; |
| 107 | |
| 108 | if records.is_empty() { |
| 109 | return CatchupResult::Idle; |
| 110 | } |
| 111 | |
| 112 | let mut dispatched = 0usize; |
| 113 | let mut max_lsn = catchup_lsn; |
| 114 | |
| 115 | for record in &records { |
| 116 | // Only process TimeseriesBatch records. |
| 117 | let record_type = nodedb_wal::record::RecordType::from_raw(record.logical_record_type()); |
| 118 | if record_type != Some(nodedb_wal::record::RecordType::TimeseriesBatch) { |
| 119 | max_lsn = max_lsn.max(record.header.lsn); |
| 120 | continue; |
| 121 | } |
| 122 | |
| 123 | // Deserialize WAL payload: (collection, raw_ilp_bytes). |
| 124 | let Ok((collection, payload)): Result<(String, Vec<u8>), _> = |
| 125 | zerompk::from_msgpack(&record.payload) |
| 126 | else { |
| 127 | max_lsn = max_lsn.max(record.header.lsn); |
| 128 | continue; |
| 129 | }; |
| 130 | |
| 131 | let tenant_id = TenantId::new(record.header.tenant_id); |
| 132 | let vshard_id = VShardId::new(record.header.vshard_id); |
| 133 | |
| 134 | let plan = PhysicalPlan::Timeseries(TimeseriesOp::Ingest { |
| 135 | collection, |
| 136 | payload, |
| 137 | format: "ilp".to_string(), |
| 138 | wal_lsn: Some(record.header.lsn), |
| 139 | // Re-derived on the engine side during apply (record carries |
| 140 | // raw ILP — row identities are reconstructed from the wire). |
| 141 | surrogates: Vec::new(), |
| 142 | }); |
| 143 |
nothing calls this directly
no test coverage detected