Append a record to the WAL. Returns the assigned LSN. The record is written to the in-memory buffer. Call `sync()` to flush to disk and make the write durable. `database_id` is stored in header bytes 34-41. Pass `0` for the default database (backward-compatible with pre-existing records).
(
&mut self,
record_type: u32,
tenant_id: u64,
vshard_id: u32,
database_id: u64,
payload: &[u8],
)
| 272 | /// `database_id` is stored in header bytes 34-41. Pass `0` for the |
| 273 | /// default database (backward-compatible with pre-existing records). |
| 274 | pub fn append( |
| 275 | &mut self, |
| 276 | record_type: u32, |
| 277 | tenant_id: u64, |
| 278 | vshard_id: u32, |
| 279 | database_id: u64, |
| 280 | payload: &[u8], |
| 281 | ) -> Result<u64> { |
| 282 | if self.sealed { |
| 283 | return Err(WalError::Sealed); |
| 284 | } |
| 285 | |
| 286 | let lsn = self.next_lsn.fetch_add(1, Ordering::Relaxed); |
| 287 | let preamble_bytes = self.segment_preamble.as_ref().map(|p| p.to_bytes()); |
| 288 | let record = WalRecord::new( |
| 289 | record_type, |
| 290 | lsn, |
| 291 | tenant_id, |
| 292 | vshard_id, |
| 293 | database_id, |
| 294 | payload.to_vec(), |
| 295 | self.encryption_ring.as_ref().map(|r| r.current()), |
| 296 | preamble_bytes.as_ref(), |
| 297 | )?; |
| 298 | |
| 299 | // Write to double-write buffer (deferred — no fsync yet). |
| 300 | // The DWB is fsynced in batch during `sync()`, before the WAL fsync. |
| 301 | // This amortizes DWB fsync cost across the entire group commit batch. |
| 302 | // |
| 303 | // DWB failure is non-fatal for the write itself (the WAL is the |
| 304 | // authoritative store), but we log a warning because it means |
| 305 | // torn-write recovery is degraded. If the DWB is persistently |
| 306 | // broken, we detach it to avoid repeated error noise. |
| 307 | if let Some(dwb) = &mut self.double_write |
| 308 | && let Err(e) = dwb.write_record_deferred(&record) |
| 309 | { |
| 310 | tracing::warn!( |
| 311 | lsn = lsn, |
| 312 | error = %e, |
| 313 | "DWB write failed — torn-write protection degraded, detaching DWB" |
| 314 | ); |
| 315 | self.double_write = None; |
| 316 | } |
| 317 | |
| 318 | let header_bytes = record.header.to_bytes(); |
| 319 | let total_size = HEADER_SIZE + record.payload.len(); |
| 320 | |
| 321 | // If this record doesn't fit in the remaining buffer, flush first. |
| 322 | if self.buffer.remaining() < total_size { |
| 323 | self.flush_buffer()?; |
| 324 | } |
| 325 | |
| 326 | // If the record is larger than the entire buffer, we have a problem. |
| 327 | // This shouldn't happen with MAX_WAL_PAYLOAD_SIZE checks, but guard anyway. |
| 328 | if total_size > self.buffer.capacity() { |
| 329 | return Err(WalError::PayloadTooLarge { |
| 330 | size: record.payload.len(), |
| 331 | max: self.buffer.capacity() - HEADER_SIZE, |