Skip to main content

Durability

Enum Durability 

Source
#[repr(u8)]
pub enum Durability { Durable = 1, Buffered = 3, }
Expand description

Re-export of weir_core::Durability so the common producer path needs a single crate import (weir_client::Durability). Durability tier requested by the producer for a given record.

There are two tiers because there are two behaviours. Durable writes and fdatasyncs the record — as part of one batch-boundary group fsync — before the ACK, so an ack means the bytes are on stable storage and will replay after a crash. Buffered trades that for latency: it acks after the in-memory write, before any fsync, so a Buffered ack survives a process crash but not power loss.

§Wire values

0x01 and 0x03 are fixed and must not change without a WIRE_VERSION bump. 0x02 is retired and permanently reserved: it was Batched, which fsynced once per batch while Sync fsynced once per record. Both moved to the batch-boundary group fsync, at which point the two tiers became the same thing. try_from still accepts 0x02 so producers built against weir 1.x keep working; the encoder only ever emits 0x01. Reusing 0x02 for a future tier would turn every 1.x client into a silent mis-tier, so it stays reserved.

Variants§

§

Durable = 1

Durable before ACK: written and fdatasynced via the batch-boundary group fsync before the ACK is sent.

§Evidence under power loss

5,311 simulated power-loss episodes over 24 hours, 42,814,591 acknowledged records, zero lost (docs/benchmarks/chaos-phase2/2026-08-28-first-power-loss-measurement.md). Zero I1 violations, zero I2 leaks, zero ledger conflicts, and the WAB was completely empty at shutdown — nothing quarantined, nothing unconfirmed, nothing dead-lettered.

The fault is a dm-flakey drop_writes device that discards writes while reporting fsync success, applied after the daemon is killed so the loss window contains only data that was dirty at the instant of death — the definition of power loss.

Each episode carried an independent check that the fault actually fired: a known block written before the fault, overwritten while it was engaged, and read back after the remount. It was destroyed in all 5,311. This matters more here than for Buffered: a tier that loses nothing cannot use “something went missing” as proof the fault was real, so without that check the result would be unfalsifiable.

Zero violations across 5,311 episodes bounds the per-episode violation rate at roughly 0.06% with 95% confidence. It is not proof of correctness — no finite run is — but it is the strongest evidence behind weir’s central claim.

§

Buffered = 3

Memory write only. ACK is sent after the record enters the in-memory queue — survives a process crash, but not power loss.

§How much is at risk

Measured, not estimated: 706 real power-loss episodes on ext4 over dm-flakey, 604,485,602 acknowledged records (docs/benchmarks/chaos-phase2/2026-08-28-first-power-loss-measurement.md).

Loss is uniformly distributed between zero and one writeback interval — a power cut lands at a random point in a periodic writeback cycle, so what has not yet reached the platter is uniform between “just flushed” and “about to flush”. The ceiling, not the mean, is what characterises the tier.

On that hardware, at ~72,000 records/s, the ceiling was 1.76 seconds of acknowledged writes (126,782 records) with a median of 0.89 s.

What generalises is the shape: the bound is one writeback interval of whatever your throughput is, and it does not grow with uptime, queue depth, or how long the process has been running. The specific figures are a property of that filesystem, its mount options and that record rate — measure your own if the number matters to you.

§WAB compression raises this materially

The figures above are for the default uncompressed WAB. A 40-episode controlled comparison — identical in every parameter except wab_compression — found that enabling zstd makes the loss bimodal rather than uniform, and roughly triples the ceiling: 449,876 records (~5.7 s) against 124,897 (~1.8 s). The two modes separate cleanly, with no episode landing between 127,711 and 307,263, and the lower mode reproduces the uncompressed distribution almost exactly. Compression packs far more records behind each unflushed byte, so a power cut takes proportionally more of them.

Correctness is unaffected — zero I1 and I2 violations, and recovery quarantined torn compressed segments at the same rate as uncompressed ones. This is a change in how much the tier can lose, not in whether it keeps its contract.

Treat the multiplier as indicative, not as a number to plan with: the chaos load generator’s payload is far more compressible than real data, so the mechanism is demonstrated but its size on a realistic workload is unmeasured.

Durable is not exposed this way by construction: it fdatasyncs at the batch boundary before acking. Note the run above was single-tier (tier="U"), so it measured Buffered only and is not itself evidence about Durable.

Implementations§

Source§

impl Durability

Source

pub const Sync: Durability = Self::Durable

👎Deprecated since 2.0.0:

renamed to Durable

Former name of Durability::Durable.

Source

pub const Batched: Durability = Self::Durable

👎Deprecated since 2.0.0:

Batched was always identical to Sync; use Durable

Former tier that was identical to Sync in behaviour. See the type docs.

Trait Implementations§

Source§

impl Clone for Durability

Source§

fn clone(&self) -> Durability

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Copy for Durability

Source§

impl Debug for Durability

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>

Formats the value using the given formatter. Read more
Source§

impl Display for Durability

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>

Also the value of the tier label on weir_records_{accepted,ack,nack}_total.

Source§

impl Eq for Durability

Source§

impl PartialEq for Durability

Source§

fn eq(&self, other: &Durability) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl StructuralPartialEq for Durability

Source§

impl TryFrom<u8> for Durability

Source§

fn try_from(value: u8) -> Result<Durability, <Durability as TryFrom<u8>>::Error>

Permissive by design: 0x02 (retired Batched) decodes to Durability::Durable so weir 1.x producers keep working.

Source§

type Error = UnknownDurability

The type returned in the event of a conversion error.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T> ToString for T
where T: Display + ?Sized,

Source§

fn to_string(&self) -> String

Converts the given value to a String. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.