Skip to main content

spate_datagen/
lane.rs

1//! The data plane: one lane per partition, generating into a reused arena.
2//!
3//! # Buffers
4//!
5//! A lane owns one `Vec<u8>` arena and one `Vec<Item>` of spans into it, both
6//! cleared and refilled per poll and neither reallocated once they have grown
7//! to a batch. Payloads borrow the arena for the batch's lifetime (ADR-0013),
8//! so a generated record costs no copy on its way into the chain.
9//!
10//! Each item's key is the order id in decimal ASCII, written into the same
11//! arena immediately before its value. All three events of an order carry that
12//! same key, so [`KeyHashRouter`](spate_core::sink::KeyHashRouter) hashes them
13//! to one shard.
14//!
15//! # The rate gate
16//!
17//! With a non-zero `tick_interval` a lane releases `events_per_tick` per
18//! cadence and parks once the quota is spent. The next deadline is the previous
19//! one plus the interval, so a slow poll does not push the whole schedule out.
20//!
21//! A tick's quota is spread over as many polls as it takes. `max_records`
22//! caps a single batch, so an `events_per_tick` above it is released across
23//! several polls of the same tick rather than truncated. The release rate is
24//! `partitions × events_per_tick ÷ tick_interval` at any `events_per_tick`.
25//!
26//! A lane polled a whole interval late counts an overrun and re-anchors to the
27//! present; missed ticks are not replayed. A `tick_interval` large enough to
28//! overflow an `Instant` leaves no next deadline, and the lane parks for good.
29//!
30//! With `tick_interval: 0s` there is no gate at all: the lane fills whatever
31//! the caller asked for and lets backpressure set the pace.
32
33use crate::encode::Encoder;
34use crate::metrics::LaneCounters;
35use crate::plan::EventPlan;
36use spate_core::checkpoint::{AckIssuer, AckRef};
37use spate_core::error::SourceError;
38use spate_core::record::{PartitionId, RawPayload};
39use spate_core::source::{LaneId, PayloadBatch, SourceLane};
40use std::io::Write;
41use std::ops::Range;
42use std::sync::Arc;
43use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
44use std::time::{Duration, Instant};
45
46/// Per-lane state the control plane reads without touching a lane.
47///
48/// One `Arc<Shared>` is cloned into every lane. A lane writes only its own
49/// index and the control plane only reads, so nothing on the record path
50/// contends on it. Each value is published on its own; a read across several
51/// of them is not a snapshot of one instant.
52#[derive(Debug)]
53pub(crate) struct Shared {
54    /// Set by a lane that has reached its budget *and been polled again*.
55    pub(crate) exhausted: Box<[AtomicBool]>,
56    /// Set by the control plane's `pause`/`resume`.
57    pub(crate) paused: Box<[AtomicBool]>,
58    /// Events left in the lane's budget, seeded with the budget itself so it
59    /// reads correctly before the lane's first fill. Zero when unbounded.
60    pub(crate) remaining: Box<[AtomicU64]>,
61    /// Orders the lane has placed and not yet captured.
62    pub(crate) open: Box<[AtomicU64]>,
63}
64
65impl Shared {
66    /// Per-lane state for `partitions` lanes. `budgets` seeds `remaining`; pass
67    /// `None` for an unbounded stream, which has no remainder to report.
68    pub(crate) fn new(partitions: usize, budgets: Option<&[u64]>) -> Shared {
69        let flags = || (0..partitions).map(|_| AtomicBool::new(false)).collect();
70        Shared {
71            exhausted: flags(),
72            paused: flags(),
73            remaining: (0..partitions)
74                .map(|i| AtomicU64::new(budgets.map_or(0, |b| b[i])))
75                .collect(),
76            open: (0..partitions).map(|_| AtomicU64::new(0)).collect(),
77        }
78    }
79}
80
81/// One payload's place in the lane's arena, plus the metadata that travels
82/// with it.
83#[derive(Debug)]
84struct Item {
85    key: Range<usize>,
86    value: Range<usize>,
87    offset: i64,
88    timestamp_ms: i64,
89}
90
91/// Everything a lane is built from.
92pub(crate) struct LaneParts {
93    pub(crate) id: LaneId,
94    pub(crate) index: usize,
95    pub(crate) issuer: AckIssuer,
96    pub(crate) plan: EventPlan,
97    pub(crate) encoder: Arc<Encoder>,
98    pub(crate) counters: Option<LaneCounters>,
99    pub(crate) shared: Arc<Shared>,
100    pub(crate) budget: u64,
101    pub(crate) tick_interval: Duration,
102    pub(crate) events_per_tick: usize,
103}
104
105/// One partition's data plane.
106#[derive(Debug)]
107pub struct DatagenLane {
108    id: LaneId,
109    partition: PartitionId,
110    index: usize,
111    issuer: AckIssuer,
112    plan: EventPlan,
113    encoder: Arc<Encoder>,
114    counters: Option<LaneCounters>,
115    shared: Arc<Shared>,
116    /// Events this lane may ever release. `u64::MAX` when unbounded.
117    budget: u64,
118    emitted: u64,
119    next_offset: i64,
120    tick_interval: Duration,
121    events_per_tick: usize,
122    next_tick: Instant,
123    /// Events left in the current tick's quota, spent across as many polls as
124    /// `max_records` takes. Always 0 when unthrottled.
125    tick_budget: usize,
126    arena: Vec<u8>,
127    items: Vec<Item>,
128}
129
130impl DatagenLane {
131    pub(crate) fn new(parts: LaneParts) -> DatagenLane {
132        DatagenLane {
133            id: parts.id,
134            partition: PartitionId(parts.index as u32),
135            index: parts.index,
136            issuer: parts.issuer,
137            plan: parts.plan,
138            encoder: parts.encoder,
139            counters: parts.counters,
140            shared: parts.shared,
141            budget: parts.budget,
142            emitted: 0,
143            next_offset: 0,
144            tick_interval: parts.tick_interval,
145            events_per_tick: parts.events_per_tick,
146            // The first tick is due immediately: the first poll releases a
147            // batch.
148            next_tick: Instant::now(),
149            tick_budget: 0,
150            arena: Vec::new(),
151            items: Vec::new(),
152        }
153    }
154
155    /// How many events this poll may release, or `None` when the current
156    /// tick's quota is spent and the next is not yet due (the lane has already
157    /// parked).
158    fn rate_gate(&mut self, timeout: Duration) -> Option<usize> {
159        if self.tick_interval.is_zero() {
160            return Some(usize::MAX);
161        }
162        // What this tick has left. Spending it over several polls is what
163        // keeps the release rate right when `max_records` caps a batch below
164        // `events_per_tick`.
165        if self.tick_budget > 0 {
166            return Some(self.tick_budget);
167        }
168        let now = Instant::now();
169        if now < self.next_tick {
170            park(Duration::min(self.next_tick - now, timeout));
171            return None;
172        }
173        let mut next = self.next_tick.checked_add(self.tick_interval);
174        if next.is_some_and(|at| at <= now) {
175            // A whole interval was already gone by the time we were polled.
176            // Re-anchor; missed ticks are not replayed.
177            if let Some(counters) = &self.counters {
178                counters.tick_overruns.increment(1);
179            }
180            next = now.checked_add(self.tick_interval);
181        }
182        // No representable next deadline: the cadence never comes due again.
183        // Parking here rather than firing is what keeps an interval past
184        // `Instant`'s range throttled instead of unthrottled.
185        let Some(next) = next else {
186            park(timeout);
187            return None;
188        };
189        self.next_tick = next;
190        if let Some(counters) = &self.counters {
191            counters.ticks.increment(1);
192        }
193        self.tick_budget = self.events_per_tick;
194        Some(self.tick_budget)
195    }
196
197    /// Generate `count` events into the arena.
198    fn fill(&mut self, count: usize) -> Result<(), SourceError> {
199        self.arena.clear();
200        self.items.clear();
201        let mut generated = [0u64; 3];
202        for _ in 0..count {
203            let (event, timestamp_ms) = self.plan.next();
204
205            let key_start = self.arena.len();
206            // Writing to a `Vec<u8>` is infallible; the `io::Write` signature
207            // is not, and an `expect` here would be a panic on the record path.
208            let _ = write!(self.arena, "{}", event.order_id());
209            let key = key_start..self.arena.len();
210
211            let value_start = self.arena.len();
212            self.encoder.encode(&event, &mut self.arena)?;
213            let value = value_start..self.arena.len();
214
215            generated[crate::metrics::kind(&event)] += 1;
216            self.items.push(Item {
217                key,
218                value,
219                offset: self.next_offset,
220                timestamp_ms,
221            });
222            self.next_offset += 1;
223            self.emitted += 1;
224        }
225        if let Some(counters) = &self.counters {
226            counters.add_generated(generated);
227        }
228        // Publish for the control plane's gauges, at the batch boundary.
229        self.shared.remaining[self.index]
230            .store(self.budget.saturating_sub(self.emitted), Ordering::Release);
231        self.shared.open[self.index].store(self.plan.open_orders(), Ordering::Release);
232        Ok(())
233    }
234}
235
236impl SourceLane for DatagenLane {
237    type Batch<'a> = DatagenBatch<'a>;
238
239    fn id(&self) -> LaneId {
240        self.id
241    }
242
243    fn partition(&self) -> PartitionId {
244        self.partition
245    }
246
247    fn poll(
248        &mut self,
249        max_records: usize,
250        timeout: Duration,
251    ) -> Result<Option<DatagenBatch<'_>>, SourceError> {
252        if self.emitted >= self.budget {
253            // Exhaustion is declared on the poll after the last batch, which
254            // is what `SourceEvent::Drained`'s contract asks for: the owning
255            // thread runs poll -> push -> poll, so reaching this branch proves
256            // the previous batch was consumed and nothing unemitted is left.
257            self.shared.exhausted[self.index].store(true, Ordering::Release);
258            park(timeout);
259            return Ok(None);
260        }
261        if self.shared.paused[self.index].load(Ordering::Acquire) || max_records == 0 {
262            park(timeout);
263            return Ok(None);
264        }
265        // Checked before the gate, so a poll that could not have used its
266        // quota does not consume a tick.
267        let Some(quota) = self.rate_gate(timeout) else {
268            return Ok(None);
269        };
270
271        let count = quota
272            .min(max_records)
273            .min(usize::try_from(self.budget - self.emitted).unwrap_or(usize::MAX));
274        self.fill(count)?;
275        self.tick_budget = self.tick_budget.saturating_sub(count);
276
277        let last_offset = self.next_offset - 1;
278        Ok(Some(DatagenBatch {
279            arena: &self.arena,
280            items: &self.items,
281            next: 0,
282            partition: self.partition,
283            ack: self.issuer.issue(self.partition, last_offset),
284        }))
285    }
286}
287
288/// One poll's payloads, borrowing the lane's arena.
289#[derive(Debug)]
290pub struct DatagenBatch<'a> {
291    arena: &'a [u8],
292    items: &'a [Item],
293    next: usize,
294    partition: PartitionId,
295    ack: AckRef,
296}
297
298impl<'a> PayloadBatch<'a> for DatagenBatch<'a> {
299    fn next_payload(&mut self) -> Option<RawPayload<'a>> {
300        let item = self.items.get(self.next)?;
301        self.next += 1;
302        Some(RawPayload {
303            bytes: &self.arena[item.value.clone()],
304            key: Some(&self.arena[item.key.clone()]),
305            partition: self.partition,
306            offset: item.offset,
307            timestamp_ms: item.timestamp_ms,
308        })
309    }
310
311    fn ack(&self) -> &AckRef {
312        &self.ack
313    }
314}
315
316/// Block the calling thread for `how_long`. Neither a lane nor the control
317/// plane may busy-spin when it has nothing to hand over.
318pub(crate) fn park(how_long: Duration) {
319    if !how_long.is_zero() {
320        std::thread::sleep(how_long);
321    }
322}