1use spate_core::checkpoint::AckIssuer;
11use spate_core::error::{ErrorClass, SourceError};
12use spate_core::record::{PartitionId, RawPayload};
13use spate_core::source::{
14 DrainBarrier, LaneId, PayloadBatch, Source, SourceCtx, SourceEvent, SourceLane,
15};
16use std::collections::{BTreeSet, HashMap, VecDeque};
17use std::sync::{Arc, Condvar, Mutex, MutexGuard};
18use std::time::{Duration, Instant};
19
20#[derive(Debug)]
22struct PayloadData {
23 key: Option<Vec<u8>>,
24 bytes: Vec<u8>,
25 offset: i64,
26 timestamp_ms: i64,
27}
28
29#[derive(Debug, Default)]
30struct PartitionQueue {
31 queue: VecDeque<PayloadData>,
32 next_offset: i64,
33}
34
35#[derive(Debug)]
36enum ScriptEvent {
37 Assign(Vec<(LaneId, PartitionId)>),
38 Revoke {
39 lanes: Vec<LaneId>,
40 barrier: DrainBarrier,
41 },
42}
43
44#[derive(Debug, Default)]
45struct Shared {
46 partitions: HashMap<PartitionId, PartitionQueue>,
47 active: HashMap<LaneId, PartitionId>,
48 paused: BTreeSet<LaneId>,
49 committed: Vec<(PartitionId, i64)>,
50 flush_commits_calls: usize,
51 open: bool,
52 events: VecDeque<ScriptEvent>,
53}
54
55#[derive(Debug, Default)]
56struct SharedSync {
57 state: Mutex<Shared>,
58 data_cv: Condvar,
60 event_cv: Condvar,
62 commit_cv: Condvar,
64}
65
66impl SharedSync {
67 fn lock(&self) -> MutexGuard<'_, Shared> {
68 self.state.lock().expect("spate-test source state poisoned")
69 }
70}
71
72#[must_use]
74pub fn memory_source() -> (MemorySource, SourceHandle) {
75 let shared = Arc::new(SharedSync::default());
76 (
77 MemorySource {
78 shared: Arc::clone(&shared),
79 issuer: None,
80 },
81 SourceHandle { shared },
82 )
83}
84
85#[derive(Clone, Debug)]
88pub struct SourceHandle {
89 shared: Arc<SharedSync>,
90}
91
92impl SourceHandle {
93 pub fn assign_lanes(&self, lanes: &[(LaneId, PartitionId)]) {
103 let mut st = self.shared.lock();
104 for &(lane, partition) in lanes {
105 assert!(
106 !st.active.contains_key(&lane),
107 "assign_lanes: lane {lane:?} is already active"
108 );
109 assert!(
110 !st.active.values().any(|&p| p == partition),
111 "assign_lanes: partition {partition:?} is already served by an active lane"
112 );
113 st.active.insert(lane, partition);
114 st.partitions.entry(partition).or_default();
115 }
116 let seen: BTreeSet<_> = lanes.iter().map(|&(l, _)| l).collect();
117 assert_eq!(seen.len(), lanes.len(), "assign_lanes: duplicate lane ids");
118 st.events.push_back(ScriptEvent::Assign(lanes.to_vec()));
119 drop(st);
120 self.shared.event_cv.notify_all();
121 }
122
123 pub fn revoke_lanes(&self, lanes: &[LaneId]) -> DrainBarrierProbe {
132 let barrier = DrainBarrier::new(lanes.len());
133 let mut st = self.shared.lock();
134 for lane in lanes {
135 assert!(
136 st.active.remove(lane).is_some(),
137 "revoke_lanes: lane {lane:?} is not active"
138 );
139 st.paused.remove(lane);
140 }
141 st.events.push_back(ScriptEvent::Revoke {
142 lanes: lanes.to_vec(),
143 barrier: barrier.clone(),
144 });
145 drop(st);
146 self.shared.event_cv.notify_all();
147 DrainBarrierProbe { barrier }
148 }
149
150 pub fn push(&self, partition: PartitionId, key: Option<&[u8]>, payload: &[u8]) -> i64 {
154 self.push_at(partition, key, payload, None)
155 }
156
157 pub fn push_at(
159 &self,
160 partition: PartitionId,
161 key: Option<&[u8]>,
162 payload: &[u8],
163 timestamp_ms: Option<i64>,
164 ) -> i64 {
165 let mut st = self.shared.lock();
166 let q = st.partitions.entry(partition).or_default();
167 let offset = q.next_offset;
168 q.next_offset += 1;
169 q.queue.push_back(PayloadData {
170 key: key.map(<[u8]>::to_vec),
171 bytes: payload.to_vec(),
172 offset,
173 timestamp_ms: timestamp_ms.unwrap_or(offset),
174 });
175 drop(st);
176 self.shared.data_cv.notify_all();
177 offset
178 }
179
180 pub fn push_many<I, P>(&self, partition: PartitionId, payloads: I) -> Vec<i64>
182 where
183 I: IntoIterator<Item = P>,
184 P: AsRef<[u8]>,
185 {
186 payloads
187 .into_iter()
188 .map(|p| self.push(partition, None, p.as_ref()))
189 .collect()
190 }
191
192 #[must_use]
195 pub fn committed(&self) -> Vec<(PartitionId, i64)> {
196 self.shared.lock().committed.clone()
197 }
198
199 #[must_use]
201 pub fn last_committed(&self, partition: PartitionId) -> Option<i64> {
202 self.shared
203 .lock()
204 .committed
205 .iter()
206 .rev()
207 .find(|&&(p, _)| p == partition)
208 .map(|&(_, o)| o)
209 }
210
211 #[must_use]
219 pub fn wait_committed(&self, partition: PartitionId, offset: i64, timeout: Duration) -> bool {
220 let deadline = Instant::now() + timeout;
221 let mut st = self.shared.lock();
222 loop {
223 let reached = st
224 .committed
225 .iter()
226 .rev()
227 .find(|&&(p, _)| p == partition)
228 .is_some_and(|&(_, o)| o >= offset);
229 if reached {
230 return true;
231 }
232 let now = Instant::now();
233 if now >= deadline {
234 return false;
235 }
236 let (guard, _) = self
237 .shared
238 .commit_cv
239 .wait_timeout(st, deadline - now)
240 .expect("spate-test source state poisoned");
241 st = guard;
242 }
243 }
244
245 #[must_use]
247 pub fn paused_lanes(&self) -> Vec<LaneId> {
248 self.shared.lock().paused.iter().copied().collect()
249 }
250
251 #[must_use]
253 pub fn flush_commits_calls(&self) -> usize {
254 self.shared.lock().flush_commits_calls
255 }
256
257 #[must_use]
259 pub fn is_open(&self) -> bool {
260 self.shared.lock().open
261 }
262}
263
264#[derive(Clone, Debug)]
266pub struct DrainBarrierProbe {
267 barrier: DrainBarrier,
268}
269
270impl DrainBarrierProbe {
271 #[must_use]
273 pub fn completed(&self) -> bool {
274 self.barrier.remaining() == 0
275 }
276
277 #[must_use]
279 pub fn remaining(&self) -> usize {
280 self.barrier.remaining()
281 }
282
283 #[must_use]
286 pub fn wait(&self, timeout: Duration) -> bool {
287 self.barrier.wait(timeout)
288 }
289}
290
291#[derive(Debug)]
293pub struct MemorySource {
294 shared: Arc<SharedSync>,
295 issuer: Option<AckIssuer>,
296}
297
298impl Source for MemorySource {
299 type Lane = MemoryLane;
300
301 fn component_type(&self) -> &str {
303 "memory"
304 }
305
306 fn open(&mut self, ctx: SourceCtx) -> Result<(), SourceError> {
307 if let Some(meter) = &ctx.meter {
310 meter.counter("opens_total", &[]).increment(1);
311 }
312 self.issuer = Some(ctx.issuer);
313 self.shared.lock().open = true;
314 Ok(())
315 }
316
317 fn poll_events(&mut self, timeout: Duration) -> Result<SourceEvent<MemoryLane>, SourceError> {
318 let issuer = self.issuer.as_ref().ok_or_else(|| SourceError::Client {
319 class: ErrorClass::Fatal,
320 reason: "poll_events called before open()".to_owned(),
321 })?;
322
323 let deadline = Instant::now() + timeout;
324 let mut st = self.shared.lock();
325 let event = loop {
326 if let Some(event) = st.events.pop_front() {
327 break event;
328 }
329 let now = Instant::now();
330 if now >= deadline {
331 return Ok(SourceEvent::Idle);
332 }
333 let (guard, _) = self
334 .shared
335 .event_cv
336 .wait_timeout(st, deadline - now)
337 .expect("spate-test source state poisoned");
338 st = guard;
339 };
340 drop(st);
341
342 Ok(match event {
343 ScriptEvent::Assign(pairs) => SourceEvent::LanesAssigned(
344 pairs
345 .into_iter()
346 .map(|(id, partition)| MemoryLane {
347 id,
348 partition,
349 shared: Arc::clone(&self.shared),
350 issuer: issuer.clone(),
351 buf: Vec::new(),
352 })
353 .collect(),
354 ),
355 ScriptEvent::Revoke { lanes, barrier } => SourceEvent::LanesRevoked { lanes, barrier },
356 })
357 }
358
359 fn commit(&mut self, watermarks: &[(PartitionId, i64)]) -> Result<(), SourceError> {
360 self.shared.lock().committed.extend_from_slice(watermarks);
361 self.shared.commit_cv.notify_all();
362 Ok(())
363 }
364
365 fn flush_commits(&mut self) -> Result<(), SourceError> {
366 self.shared.lock().flush_commits_calls += 1;
367 Ok(())
368 }
369
370 fn pause(&mut self, lanes: &[LaneId]) -> Result<(), SourceError> {
371 let mut st = self.shared.lock();
372 for lane in lanes {
373 assert!(
374 st.active.contains_key(lane),
375 "pause: lane {lane:?} is not active — runtime bug"
376 );
377 st.paused.insert(*lane);
378 }
379 Ok(())
380 }
381
382 fn resume(&mut self, lanes: &[LaneId]) -> Result<(), SourceError> {
383 let mut st = self.shared.lock();
384 for lane in lanes {
385 assert!(
386 st.active.contains_key(lane),
387 "resume: lane {lane:?} is not active — runtime bug"
388 );
389 st.paused.remove(lane);
390 }
391 drop(st);
392 self.shared.data_cv.notify_all();
393 Ok(())
394 }
395}
396
397#[derive(Debug)]
401pub struct MemoryLane {
402 id: LaneId,
403 partition: PartitionId,
404 shared: Arc<SharedSync>,
405 issuer: AckIssuer,
406 buf: Vec<PayloadData>,
407}
408
409impl SourceLane for MemoryLane {
410 type Batch<'a> = MemoryBatch<'a>;
411
412 fn id(&self) -> LaneId {
413 self.id
414 }
415
416 fn partition(&self) -> PartitionId {
417 self.partition
418 }
419
420 fn poll(
421 &mut self,
422 max_records: usize,
423 timeout: Duration,
424 ) -> Result<Option<MemoryBatch<'_>>, SourceError> {
425 if max_records == 0 {
426 return Ok(None);
427 }
428 let deadline = Instant::now() + timeout;
429 {
430 let mut st = self.shared.lock();
431 self.buf = loop {
432 let available = !st.paused.contains(&self.id)
433 && st
434 .partitions
435 .get(&self.partition)
436 .is_some_and(|q| !q.queue.is_empty());
437 if available {
438 let q = st
439 .partitions
440 .get_mut(&self.partition)
441 .expect("partition queue exists");
442 let n = q.queue.len().min(max_records);
443 break q.queue.drain(..n).collect();
444 }
445 let now = Instant::now();
446 if now >= deadline {
447 return Ok(None);
448 }
449 let (guard, _) = self
450 .shared
451 .data_cv
452 .wait_timeout(st, deadline - now)
453 .expect("spate-test source state poisoned");
454 st = guard;
455 };
456 }
457 let last_offset = self.buf.last().expect("non-empty batch").offset;
458 let ack = self.issuer.issue(self.partition, last_offset);
459 Ok(Some(MemoryBatch {
460 items: &self.buf,
461 next: 0,
462 partition: self.partition,
463 ack,
464 }))
465 }
466}
467
468#[derive(Debug)]
470pub struct MemoryBatch<'a> {
471 items: &'a [PayloadData],
472 next: usize,
473 partition: PartitionId,
474 ack: spate_core::checkpoint::AckRef,
475}
476
477impl<'a> PayloadBatch<'a> for MemoryBatch<'a> {
478 fn next_payload(&mut self) -> Option<RawPayload<'a>> {
479 let item = self.items.get(self.next)?;
480 self.next += 1;
481 Some(RawPayload {
482 bytes: &item.bytes,
483 key: item.key.as_deref(),
484 partition: self.partition,
485 offset: item.offset,
486 timestamp_ms: item.timestamp_ms,
487 })
488 }
489
490 fn ack(&self) -> &spate_core::checkpoint::AckRef {
491 &self.ack
492 }
493}