1#![expect(deprecated, reason = "v0.4.0 removes this with spate-coordination")]
10
11use spate_core::coordination::ControlWaker;
12use spate_core::coordination::{
13 CoordinationError, CoordinationErrorKind, CoordinationEvent, LeaseEpoch, SplitCoordinator,
14 SplitId, SplitPlanner, SplitProgress, SplitSpec,
15};
16use std::collections::{HashMap, VecDeque};
17use std::sync::{Arc, Mutex};
18
19#[derive(Default)]
20struct State {
21 events: Vec<CoordinationEvent>,
22 commit_outcomes: HashMap<SplitId, VecDeque<CoordinationErrorKind>>,
23 commits: Vec<(SplitId, SplitProgress)>,
24 final_commits: Vec<Vec<(SplitId, SplitProgress)>>,
25 fail_outcomes: HashMap<SplitId, VecDeque<CoordinationErrorKind>>,
26 failed: Vec<(SplitId, LeaseEpoch, String)>,
27 released: Vec<SplitId>,
28 departed: bool,
29 planner: Option<Box<dyn SplitPlanner>>,
30 started: bool,
31 waker: Option<ControlWaker>,
32}
33
34#[derive(Debug)]
37#[deprecated(
38 since = "0.3.0",
39 note = "removed in v0.4.0, where a work feed replaces split coordination; see https://github.com/spate-etl/spate/issues/1088"
40)]
41pub struct ScriptedCoordinator {
42 state: Arc<Mutex<State>>,
43}
44
45#[derive(Clone, Debug)]
52#[deprecated(
53 since = "0.3.0",
54 note = "removed in v0.4.0, where a work feed replaces split coordination; see https://github.com/spate-etl/spate/issues/1088"
55)]
56pub struct CoordinatorScript {
57 state: Arc<Mutex<State>>,
58}
59
60impl std::fmt::Debug for State {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.debug_struct("State")
63 .field("pending_events", &self.events.len())
64 .field("commits", &self.commits.len())
65 .field("started", &self.started)
66 .finish_non_exhaustive()
67 }
68}
69
70#[must_use]
72#[deprecated(
73 since = "0.3.0",
74 note = "removed in v0.4.0, where a work feed replaces split coordination; see https://github.com/spate-etl/spate/issues/1088"
75)]
76pub fn scripted_coordinator() -> (ScriptedCoordinator, CoordinatorScript) {
77 let state = Arc::new(Mutex::new(State::default()));
78 (
79 ScriptedCoordinator {
80 state: Arc::clone(&state),
81 },
82 CoordinatorScript { state },
83 )
84}
85
86impl CoordinatorScript {
87 pub fn gain(&self, split: SplitSpec, epoch: u64, progress: Option<SplitProgress>) {
89 self.lock().events.push(CoordinationEvent::Gained {
90 split,
91 epoch: LeaseEpoch(epoch),
92 progress,
93 });
94 self.wake();
95 }
96
97 pub fn lose(&self, split: &SplitId) {
99 self.lock().events.push(CoordinationEvent::Lost {
100 split: split.clone(),
101 });
102 self.wake();
103 }
104
105 pub fn quarantine(&self, split: &SplitId, attempts: u32) {
107 self.lock().events.push(CoordinationEvent::Quarantined {
108 split: split.clone(),
109 attempts,
110 });
111 self.wake();
112 }
113
114 pub fn all_complete(&self) {
116 self.lock().events.push(CoordinationEvent::AllComplete);
117 self.wake();
118 }
119
120 pub fn stalled(&self, completed: u64, quarantined: u64) {
122 self.lock().events.push(CoordinationEvent::Stalled {
123 completed,
124 quarantined,
125 });
126 self.wake();
127 }
128
129 pub fn fail_next_commit(&self, split: &SplitId, kind: CoordinationErrorKind) {
132 self.lock()
133 .commit_outcomes
134 .entry(split.clone())
135 .or_default()
136 .push_back(kind);
137 }
138
139 #[must_use]
141 pub fn commits(&self) -> Vec<(SplitId, SplitProgress)> {
142 self.lock().commits.clone()
143 }
144
145 #[must_use]
147 pub fn final_commits(&self) -> Vec<Vec<(SplitId, SplitProgress)>> {
148 self.lock().final_commits.clone()
149 }
150
151 #[must_use]
153 pub fn last_commit(&self, split: &SplitId) -> Option<SplitProgress> {
154 self.lock()
155 .commits
156 .iter()
157 .rev()
158 .find(|(s, _)| s == split)
159 .map(|(_, p)| p.clone())
160 }
161
162 pub fn fail_next_report(&self, split: &SplitId, kind: CoordinationErrorKind) {
166 self.lock()
167 .fail_outcomes
168 .entry(split.clone())
169 .or_default()
170 .push_back(kind);
171 }
172
173 #[must_use]
176 pub fn failed(&self) -> Vec<(SplitId, LeaseEpoch, String)> {
177 self.lock().failed.clone()
178 }
179
180 #[must_use]
182 pub fn released(&self) -> Vec<SplitId> {
183 self.lock().released.clone()
184 }
185
186 #[must_use]
189 pub fn departed(&self) -> bool {
190 self.lock().departed
191 }
192
193 #[must_use]
195 pub fn started(&self) -> bool {
196 self.lock().started
197 }
198
199 #[must_use]
204 pub fn take_planner(&self) -> Option<Box<dyn SplitPlanner>> {
205 self.lock().planner.take()
206 }
207
208 fn lock(&self) -> std::sync::MutexGuard<'_, State> {
209 self.state.lock().expect("coordinator script poisoned")
210 }
211
212 fn wake(&self) {
215 if let Some(w) = &self.lock().waker {
216 w.wake();
217 }
218 }
219}
220
221impl SplitCoordinator for ScriptedCoordinator {
222 fn start(&mut self, planner: Box<dyn SplitPlanner>) -> Result<(), CoordinationError> {
223 let mut state = self.state.lock().expect("coordinator script poisoned");
224 state.planner = Some(planner);
225 state.started = true;
226 Ok(())
227 }
228
229 fn set_waker(&mut self, waker: ControlWaker) {
230 self.state
231 .lock()
232 .expect("coordinator script poisoned")
233 .waker = Some(waker);
234 }
235
236 fn poll(&mut self) -> Result<Vec<CoordinationEvent>, CoordinationError> {
237 Ok(std::mem::take(
240 &mut self
241 .state
242 .lock()
243 .expect("coordinator script poisoned")
244 .events,
245 ))
246 }
247
248 fn commit(
249 &mut self,
250 split: &SplitId,
251 progress: &SplitProgress,
252 ) -> Result<(), CoordinationError> {
253 let mut state = self.state.lock().expect("coordinator script poisoned");
254 if let Some(kinds) = state.commit_outcomes.get_mut(split)
255 && let Some(kind) = kinds.pop_front()
256 {
257 return Err(CoordinationError::new(
258 kind,
259 format!("scripted {kind:?} for split {split}"),
260 ));
261 }
262 state.commits.push((split.clone(), progress.clone()));
263 Ok(())
264 }
265
266 fn commit_final(
267 &mut self,
268 commits: &[(SplitId, SplitProgress)],
269 ) -> Vec<Result<(), CoordinationError>> {
270 self.state
271 .lock()
272 .expect("coordinator script poisoned")
273 .final_commits
274 .push(commits.to_vec());
275 commits
276 .iter()
277 .map(|(split, progress)| self.commit(split, progress))
278 .collect()
279 }
280
281 fn fail(
282 &mut self,
283 split: &SplitId,
284 epoch: LeaseEpoch,
285 reason: &str,
286 ) -> Result<(), CoordinationError> {
287 let mut state = self.state.lock().expect("coordinator script poisoned");
288 state
289 .failed
290 .push((split.clone(), epoch, reason.to_string()));
291 if let Some(kinds) = state.fail_outcomes.get_mut(split)
292 && let Some(kind) = kinds.pop_front()
293 {
294 return Err(CoordinationError::new(
295 kind,
296 format!("scripted {kind:?} for split {split}"),
297 ));
298 }
299 Ok(())
300 }
301
302 fn release(&mut self, splits: &[SplitId]) -> Result<(), CoordinationError> {
303 self.state
304 .lock()
305 .expect("coordinator script poisoned")
306 .released
307 .extend(splits.iter().cloned());
308 Ok(())
309 }
310
311 fn depart(&mut self, held: &[SplitId]) -> Result<(), CoordinationError> {
312 let mut state = self.state.lock().expect("coordinator script poisoned");
313 state.departed = true;
314 state.released.extend(held.iter().cloned());
315 Ok(())
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322 use spate_core::coordination::{PlanContext, PlanFinality, SplitPlan};
323
324 struct OneSplit;
325
326 impl SplitPlanner for OneSplit {
327 fn fingerprint(&self) -> String {
328 "test:v1".into()
329 }
330
331 fn plan(&mut self, _ctx: PlanContext<'_>) -> Result<SplitPlan, CoordinationError> {
332 Ok(SplitPlan::new(
333 vec![spate_core::coordination::PlannedSplit::new(SplitSpec::new(
334 SplitId::new("only").unwrap(),
335 b"all of it".to_vec(),
336 ))],
337 PlanFinality::Final,
338 ))
339 }
340 }
341
342 #[test]
343 fn scripts_events_and_observes_interactions() {
344 let (mut coordinator, script) = scripted_coordinator();
345 coordinator.start(Box::new(OneSplit)).unwrap();
346 assert!(script.started());
347
348 let mut planner = script.take_planner().expect("planner captured");
350 let plan = planner.plan(PlanContext::new(None, 1)).unwrap();
351 assert_eq!(plan.splits.len(), 1);
352 let split = plan.splits[0].spec.clone();
353 let id = split.id.clone();
354
355 script.gain(split, 1, None);
357 script.lose(&id);
358 let batch = coordinator.poll().unwrap();
359 assert_eq!(batch.len(), 2);
360 assert!(matches!(batch[0], CoordinationEvent::Gained { .. }));
361 assert!(matches!(batch[1], CoordinationEvent::Lost { .. }));
362 assert!(coordinator.poll().unwrap().is_empty());
363
364 script.fail_next_commit(&id, CoordinationErrorKind::Retryable);
366 let progress = SplitProgress::new(5, vec![]);
367 let err = coordinator.commit(&id, &progress).unwrap_err();
368 assert_eq!(err.kind, CoordinationErrorKind::Retryable);
369 coordinator.commit(&id, &progress).unwrap();
370 assert_eq!(script.commits().len(), 1);
371 assert_eq!(script.last_commit(&id).unwrap().watermark, 5);
372
373 coordinator.fail(&id, LeaseEpoch(1), "poison").unwrap();
374 assert_eq!(script.failed()[0].1, LeaseEpoch(1));
375 assert_eq!(script.failed()[0].2, "poison");
376 coordinator.release(std::slice::from_ref(&id)).unwrap();
377 assert_eq!(script.released(), vec![id]);
378 assert!(!script.departed());
379 coordinator.depart(&[]).unwrap();
380 assert!(script.departed());
381 }
382
383 #[test]
384 fn final_commits_records_each_batch_and_applies_scripted_outcomes() {
385 let (mut coordinator, script) = scripted_coordinator();
386 let ids: Vec<SplitId> = ["a", "b", "c"]
387 .into_iter()
388 .map(|id| SplitId::new(id).unwrap())
389 .collect();
390 let batch: Vec<(SplitId, SplitProgress)> = ids
391 .iter()
392 .zip(1..)
393 .map(|(id, w)| (id.clone(), SplitProgress::new(w, vec![])))
394 .collect();
395 script.fail_next_commit(&ids[1], CoordinationErrorKind::Retryable);
396
397 let results = coordinator.commit_final(&batch);
398
399 assert_eq!(script.final_commits(), vec![batch.clone()]);
400 assert_eq!(results.len(), 3);
401 assert!(results[0].is_ok());
402 assert_eq!(
403 results[1].as_ref().unwrap_err().kind,
404 CoordinationErrorKind::Retryable
405 );
406 assert!(results[2].is_ok());
407 assert_eq!(script.commits(), vec![batch[0].clone(), batch[2].clone()]);
408 }
409}