1use std::collections::HashMap;
12use std::marker::PhantomData;
13use std::sync::OnceLock;
14use std::time::Duration;
15
16use algebra::JoinSemilattice;
17use enum_as_inner::EnumAsInner;
18use hyperactor_config::NonZeroUsize;
19use serde::Deserialize;
20use serde::Serialize;
21use serde::de::DeserializeOwned;
22use typeuri::Named;
23
24use crate::config;
26
27pub trait Accumulator {
29 type State;
31 type Update;
34
35 fn accumulate(&self, state: &mut Self::State, update: Self::Update) -> anyhow::Result<()>;
37
38 fn reducer_spec(&self) -> Option<ReducerSpec>;
40}
41
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, typeuri::Named)]
44pub struct ReducerSpec {
45 pub typehash: u64,
47 pub builder_params: Option<wirevalue::Any>,
49}
50wirevalue::register_type!(ReducerSpec);
51
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Named, Default)]
54pub struct StreamingReducerOpts {
55 pub max_update_interval: Option<Duration>,
58 pub initial_update_interval: Option<Duration>,
62}
63
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Named)]
66pub struct IdleFlushReducerOpts {
67 pub idle_timeout: Duration,
69 pub abandon_timeout: Duration,
71 pub expected_updates_per_destination: NonZeroUsize,
73}
74
75#[derive(
77 Debug,
78 Clone,
79 PartialEq,
80 Serialize,
81 Deserialize,
82 EnumAsInner,
83 typeuri::Named
84)]
85pub enum ReducerMode {
86 Streaming(StreamingReducerOpts),
88 Once(usize),
90 IdleFlush {
93 expected: NonZeroUsize,
95 idle_timeout: Duration,
97 abandon_timeout: Duration,
99 },
100}
101
102impl Default for ReducerMode {
103 fn default() -> Self {
104 ReducerMode::Streaming(StreamingReducerOpts::default())
105 }
106}
107
108impl ReducerMode {
109 pub(crate) fn max_update_interval(&self) -> Duration {
110 match self {
111 ReducerMode::Streaming(opts) => opts
112 .max_update_interval
113 .unwrap_or(hyperactor_config::global::get(config::SPLIT_MAX_BUFFER_AGE)),
114 ReducerMode::Once(_) | ReducerMode::IdleFlush { .. } => Duration::MAX,
115 }
116 }
117
118 pub(crate) fn initial_update_interval(&self) -> Duration {
119 match self {
120 ReducerMode::Streaming(opts) => opts
121 .initial_update_interval
122 .unwrap_or(Duration::from_millis(1)),
123 ReducerMode::Once(_) | ReducerMode::IdleFlush { .. } => Duration::MAX,
124 }
125 }
126}
127
128pub trait CommReducer {
134 type Update;
136
137 fn reduce(&self, left: Self::Update, right: Self::Update) -> anyhow::Result<Self::Update>;
139}
140
141pub trait ErasedCommReducer {
143 fn reduce_erased(
145 &self,
146 left: &wirevalue::Any,
147 right: &wirevalue::Any,
148 ) -> anyhow::Result<wirevalue::Any>;
149
150 fn reduce_updates(
153 &self,
154 updates: Vec<wirevalue::Any>,
155 ) -> Result<wirevalue::Any, (anyhow::Error, Vec<wirevalue::Any>)> {
156 if updates.is_empty() {
157 return Err((anyhow::anyhow!("empty updates"), updates));
158 }
159 if updates.len() == 1 {
160 return Ok(updates.into_iter().next().expect("checked above"));
161 }
162
163 let mut iter = updates.iter();
164 let first = iter.next().unwrap();
165 let second = iter.next().unwrap();
166 let init = match self.reduce_erased(first, second) {
167 Ok(v) => v,
168 Err(e) => return Err((e, updates)),
169 };
170 let reduced = match iter.try_fold(init, |acc, e| self.reduce_erased(&acc, e)) {
171 Ok(v) => v,
172 Err(e) => return Err((e, updates)),
173 };
174 Ok(reduced)
175 }
176
177 fn typehash(&self) -> u64;
179}
180
181impl<R, T> ErasedCommReducer for R
182where
183 R: CommReducer<Update = T> + Named,
184 T: Serialize + DeserializeOwned + Named,
185{
186 fn reduce_erased(
187 &self,
188 left: &wirevalue::Any,
189 right: &wirevalue::Any,
190 ) -> anyhow::Result<wirevalue::Any> {
191 let left = left.deserialized::<T>()?;
192 let right = right.deserialized::<T>()?;
193 let result = self.reduce(left, right)?;
194 Ok(wirevalue::Any::serialize(&result)?)
195 }
196
197 fn typehash(&self) -> u64 {
198 R::typehash()
199 }
200}
201
202pub struct ReducerFactory {
207 pub typehash_f: fn() -> u64,
210 pub builder_f: fn(
212 Option<wirevalue::Any>,
213 ) -> anyhow::Result<Box<dyn ErasedCommReducer + Sync + Send + 'static>>,
214}
215
216inventory::collect!(ReducerFactory);
217
218inventory::submit! {
219 ReducerFactory {
220 typehash_f: <SumReducer<i64> as Named>::typehash,
221 builder_f: |_| Ok(Box::new(SumReducer::<i64>(PhantomData))),
222 }
223}
224inventory::submit! {
225 ReducerFactory {
226 typehash_f: <SumReducer<u64> as Named>::typehash,
227 builder_f: |_| Ok(Box::new(SumReducer::<u64>(PhantomData))),
228 }
229}
230inventory::submit! {
231 ReducerFactory {
232 typehash_f: <SemilatticeReducer<Max<i64>> as Named>::typehash,
233 builder_f: |_| Ok(Box::new(SemilatticeReducer::<Max<i64>>(PhantomData))),
234 }
235}
236inventory::submit! {
237 ReducerFactory {
238 typehash_f: <SemilatticeReducer<Max<u64>> as Named>::typehash,
239 builder_f: |_| Ok(Box::new(SemilatticeReducer::<Max<u64>>(PhantomData))),
240 }
241}
242inventory::submit! {
243 ReducerFactory {
244 typehash_f: <SemilatticeReducer<Min<i64>> as Named>::typehash,
245 builder_f: |_| Ok(Box::new(SemilatticeReducer::<Min<i64>>(PhantomData))),
246 }
247}
248inventory::submit! {
249 ReducerFactory {
250 typehash_f: <SemilatticeReducer<Min<u64>> as Named>::typehash,
251 builder_f: |_| Ok(Box::new(SemilatticeReducer::<Min<u64>>(PhantomData))),
252 }
253}
254inventory::submit! {
255 ReducerFactory {
256 typehash_f: <SemilatticeReducer<WatermarkUpdate<i64>> as Named>::typehash,
257 builder_f: |_| Ok(Box::new(SemilatticeReducer::<WatermarkUpdate<i64>>(PhantomData))),
258 }
259}
260inventory::submit! {
261 ReducerFactory {
262 typehash_f: <SemilatticeReducer<WatermarkUpdate<u64>> as Named>::typehash,
263 builder_f: |_| Ok(Box::new(SemilatticeReducer::<WatermarkUpdate<u64>>(PhantomData))),
264 }
265}
266inventory::submit! {
267 ReducerFactory {
268 typehash_f: <SemilatticeReducer<GCounterUpdate> as Named>::typehash,
269 builder_f: |_| Ok(Box::new(SemilatticeReducer::<GCounterUpdate>(PhantomData))),
270 }
271}
272inventory::submit! {
273 ReducerFactory {
274 typehash_f: <SemilatticeReducer<PNCounterUpdate> as Named>::typehash,
275 builder_f: |_| Ok(Box::new(SemilatticeReducer::<PNCounterUpdate>(PhantomData))),
276 }
277}
278inventory::submit! {
279 ReducerFactory {
280 typehash_f: <UnitReducer as Named>::typehash,
281 builder_f: |_| Ok(Box::new(UnitReducer)),
282 }
283}
284
285pub(crate) fn resolve_reducer(
288 typehash: u64,
289 builder_params: Option<wirevalue::Any>,
290) -> anyhow::Result<Option<Box<dyn ErasedCommReducer + Sync + Send + 'static>>> {
291 static FACTORY_MAP: OnceLock<HashMap<u64, &'static ReducerFactory>> = OnceLock::new();
292 let factories = FACTORY_MAP.get_or_init(|| {
293 let mut map = HashMap::new();
294 for factory in inventory::iter::<ReducerFactory> {
295 map.insert((factory.typehash_f)(), factory);
296 }
297 map
298 });
299
300 factories
301 .get(&typehash)
302 .map(|f| (f.builder_f)(builder_params))
303 .transpose()
304}
305
306#[derive(typeuri::Named)]
307struct SumReducer<T>(PhantomData<T>);
308
309impl<T: std::ops::Add<Output = T> + Copy + 'static> CommReducer for SumReducer<T> {
310 type Update = T;
311
312 fn reduce(&self, left: T, right: T) -> anyhow::Result<T> {
313 Ok(left + right)
314 }
315}
316
317struct SumAccumulator<T>(PhantomData<T>);
320
321impl<T: std::ops::Add<Output = T> + Copy + Named + 'static> Accumulator for SumAccumulator<T> {
322 type State = T;
323 type Update = T;
324
325 fn accumulate(&self, state: &mut T, update: T) -> anyhow::Result<()> {
326 *state = *state + update;
327 Ok(())
328 }
329
330 fn reducer_spec(&self) -> Option<ReducerSpec> {
331 Some(ReducerSpec {
332 typehash: <SumReducer<T> as Named>::typehash(),
333 builder_params: None,
334 })
335 }
336}
337
338pub fn sum<T: std::ops::Add<Output = T> + Copy + Named + 'static>()
363-> impl Accumulator<State = T, Update = T> {
364 SumAccumulator(PhantomData)
365}
366
367#[derive(typeuri::Named)]
369struct UnitReducer;
370
371impl CommReducer for UnitReducer {
372 type Update = ();
373
374 fn reduce(&self, _left: (), _right: ()) -> anyhow::Result<()> {
375 Ok(())
376 }
377}
378
379struct UnitAccumulator;
381
382impl Accumulator for UnitAccumulator {
383 type State = ();
384 type Update = ();
385
386 fn accumulate(&self, _state: &mut (), _update: ()) -> anyhow::Result<()> {
387 Ok(())
388 }
389
390 fn reducer_spec(&self) -> Option<ReducerSpec> {
391 Some(ReducerSpec {
392 typehash: <UnitReducer as Named>::typehash(),
393 builder_params: None,
394 })
395 }
396}
397
398pub fn unit() -> impl Accumulator<State = (), Update = ()> {
405 UnitAccumulator
406}
407
408#[derive(typeuri::Named)]
410struct SemilatticeReducer<L>(PhantomData<L>);
411
412impl<L: JoinSemilattice + Clone> CommReducer for SemilatticeReducer<L> {
413 type Update = L;
414
415 fn reduce(&self, left: L, right: L) -> anyhow::Result<L> {
416 Ok(left.join(&right))
417 }
418}
419
420struct SemilatticeAccumulator<L>(PhantomData<L>);
422
423impl<L: JoinSemilattice + Clone + Named + 'static> Accumulator for SemilatticeAccumulator<L> {
424 type State = L;
425 type Update = L;
426
427 fn accumulate(&self, state: &mut L, update: L) -> anyhow::Result<()> {
428 *state = state.join(&update);
429 Ok(())
430 }
431
432 fn reducer_spec(&self) -> Option<ReducerSpec> {
433 Some(ReducerSpec {
434 typehash: <SemilatticeReducer<L> as Named>::typehash(),
435 builder_params: None,
436 })
437 }
438}
439
440pub fn join_semilattice<L: JoinSemilattice + Clone + Named + 'static>()
454-> impl Accumulator<State = L, Update = L> {
455 SemilatticeAccumulator::<L>(PhantomData)
456}
457
458pub use algebra::Max;
460pub use algebra::Min;
462
463#[derive(Default, Debug, Clone, Serialize, Deserialize, typeuri::Named)]
485pub struct WatermarkUpdate<T>(algebra::LatticeMap<usize, algebra::LWW<T>>);
486
487impl<T: Ord + Clone> WatermarkUpdate<T> {
488 pub fn get(&self) -> &T {
493 self.0
494 .iter()
495 .map(|(_, lww)| &lww.value)
496 .min()
497 .expect("watermark should have been initialized")
498 }
499
500 pub fn get_rank(&self, rank: usize) -> Option<&T> {
502 self.0.get(&rank).map(|lww| &lww.value)
503 }
504
505 pub fn num_ranks(&self) -> usize {
507 self.0.len()
508 }
509}
510
511impl<T> From<(usize, T, u64)> for WatermarkUpdate<T> {
512 fn from((rank, value, timestamp): (usize, T, u64)) -> Self {
518 let mut map = algebra::LatticeMap::new();
519 map.insert(rank, algebra::LWW::new(value, timestamp, rank as u64));
521 Self(map)
522 }
523}
524
525impl<T: Clone + PartialEq> JoinSemilattice for WatermarkUpdate<T> {
526 fn join(&self, other: &Self) -> Self {
527 WatermarkUpdate(self.0.join(&other.0))
528 }
529}
530
531#[derive(Default, Debug, Clone, Serialize, Deserialize, typeuri::Named)]
543pub struct GCounterUpdate(algebra::LatticeMap<usize, Max<u64>>);
544wirevalue::register_type!(GCounterUpdate);
545
546impl GCounterUpdate {
547 pub fn get(&self) -> u64 {
549 self.0.iter().map(|(_, max)| max.0).sum()
550 }
551
552 pub fn get_rank(&self, rank: usize) -> Option<u64> {
554 self.0.get(&rank).map(|max| max.0)
555 }
556
557 pub fn num_ranks(&self) -> usize {
559 self.0.len()
560 }
561}
562
563impl From<(usize, u64)> for GCounterUpdate {
564 fn from((rank, count): (usize, u64)) -> Self {
566 let mut map = algebra::LatticeMap::new();
567 map.insert(rank, Max(count));
568 Self(map)
569 }
570}
571
572impl JoinSemilattice for GCounterUpdate {
573 fn join(&self, other: &Self) -> Self {
574 GCounterUpdate(self.0.join(&other.0))
575 }
576}
577
578#[derive(Default, Debug, Clone, Serialize, Deserialize, typeuri::Named)]
585pub struct PNCounterUpdate {
586 p: algebra::LatticeMap<usize, Max<u64>>,
587 n: algebra::LatticeMap<usize, Max<u64>>,
588}
589wirevalue::register_type!(PNCounterUpdate);
590
591impl PNCounterUpdate {
592 pub fn get(&self) -> i64 {
594 let p: u64 = self.p.iter().map(|(_, m)| m.0).sum();
595 let n: u64 = self.n.iter().map(|(_, m)| m.0).sum();
596 p as i64 - n as i64
597 }
598
599 pub fn inc(rank: usize, delta: u64) -> Self {
601 let mut p = algebra::LatticeMap::new();
602 p.insert(rank, Max(delta));
603 Self {
604 p,
605 n: algebra::LatticeMap::new(),
606 }
607 }
608
609 pub fn dec(rank: usize, delta: u64) -> Self {
611 let mut n = algebra::LatticeMap::new();
612 n.insert(rank, Max(delta));
613 Self {
614 p: algebra::LatticeMap::new(),
615 n,
616 }
617 }
618
619 pub fn num_inc_ranks(&self) -> usize {
621 self.p.len()
622 }
623
624 pub fn num_dec_ranks(&self) -> usize {
626 self.n.len()
627 }
628}
629
630impl JoinSemilattice for PNCounterUpdate {
631 fn join(&self, other: &Self) -> Self {
632 PNCounterUpdate {
633 p: self.p.join(&other.p),
634 n: self.n.join(&other.n),
635 }
636 }
637}
638
639#[cfg(test)]
640mod tests {
641 use std::fmt::Debug;
642
643 use maplit::hashmap;
644 use typeuri::Named;
645
646 use super::*;
647
648 fn serialize<T: Serialize + Named>(values: Vec<T>) -> Vec<wirevalue::Any> {
649 values
650 .into_iter()
651 .map(|n| wirevalue::Any::serialize(&n).unwrap())
652 .collect()
653 }
654
655 #[test]
656 fn test_comm_reducer_numeric() {
657 let u64_numbers_sum: Vec<_> = serialize(vec![1u64, 3u64, 1100u64]);
658 let i64_numbers_sum: Vec<_> = serialize(vec![-123i64, 33i64, 110i64]);
659 let u64_numbers_max: Vec<_> = serialize(vec![Max(1u64), Max(3u64), Max(1100u64)]);
660 let i64_numbers_max: Vec<_> = serialize(vec![Max(-123i64), Max(33i64), Max(110i64)]);
661 let u64_numbers_min: Vec<_> = serialize(vec![Min(1u64), Min(3u64), Min(1100u64)]);
662 let i64_numbers_min: Vec<_> = serialize(vec![Min(-123i64), Min(33i64), Min(110i64)]);
663 {
664 let typehash = <SemilatticeReducer<Max<u64>> as Named>::typehash();
665 assert_eq!(
666 resolve_reducer(typehash, None)
667 .unwrap()
668 .unwrap()
669 .reduce_updates(u64_numbers_max.clone())
670 .unwrap()
671 .deserialized::<Max<u64>>()
672 .unwrap(),
673 Max(1100u64),
674 );
675
676 let typehash = <SemilatticeReducer<Min<u64>> as Named>::typehash();
677 assert_eq!(
678 resolve_reducer(typehash, None)
679 .unwrap()
680 .unwrap()
681 .reduce_updates(u64_numbers_min.clone())
682 .unwrap()
683 .deserialized::<Min<u64>>()
684 .unwrap(),
685 Min(1u64),
686 );
687
688 let typehash = <SumReducer<u64> as Named>::typehash();
689 assert_eq!(
690 resolve_reducer(typehash, None)
691 .unwrap()
692 .unwrap()
693 .reduce_updates(u64_numbers_sum)
694 .unwrap()
695 .deserialized::<u64>()
696 .unwrap(),
697 1104u64,
698 );
699 }
700
701 {
702 let typehash = <SemilatticeReducer<Max<i64>> as Named>::typehash();
703 assert_eq!(
704 resolve_reducer(typehash, None)
705 .unwrap()
706 .unwrap()
707 .reduce_updates(i64_numbers_max.clone())
708 .unwrap()
709 .deserialized::<Max<i64>>()
710 .unwrap(),
711 Max(110i64),
712 );
713
714 let typehash = <SemilatticeReducer<Min<i64>> as Named>::typehash();
715 assert_eq!(
716 resolve_reducer(typehash, None)
717 .unwrap()
718 .unwrap()
719 .reduce_updates(i64_numbers_min.clone())
720 .unwrap()
721 .deserialized::<Min<i64>>()
722 .unwrap(),
723 Min(-123i64),
724 );
725
726 let typehash = <SumReducer<i64> as Named>::typehash();
727 assert_eq!(
728 resolve_reducer(typehash, None)
729 .unwrap()
730 .unwrap()
731 .reduce_updates(i64_numbers_sum)
732 .unwrap()
733 .deserialized::<i64>()
734 .unwrap(),
735 20i64,
736 );
737 }
738 }
739
740 #[test]
741 fn test_comm_reducer_watermark() {
742 let u64_updates = serialize::<WatermarkUpdate<u64>>(
744 vec![
745 (1, 1, 0), (0, 2, 1), (0, 1, 2), (3, 35, 3), (0, 9, 4), (1, 10, 5), (3, 32, 6), (3, 0, 7), (3, 321, 8), ]
755 .into_iter()
756 .map(|(k, v, ts)| WatermarkUpdate::from((k, v, ts)))
757 .collect(),
758 );
759 let i64_updates: Vec<_> = serialize::<WatermarkUpdate<i64>>(
760 vec![
761 (0, 2, 0), (1, 1, 1), (3, 35, 2), (0, 1, 3), (1, -10, 4), (3, 32, 5), (3, 0, 6), (3, -99, 7), (0, -9, 8), ]
771 .into_iter()
772 .map(WatermarkUpdate::from)
773 .collect(),
774 );
775
776 fn verify<T: Ord + Clone + PartialEq + DeserializeOwned + Debug + Named>(
777 updates: Vec<wirevalue::Any>,
778 expected: HashMap<usize, T>,
779 ) {
780 let typehash = <SemilatticeReducer<WatermarkUpdate<T>> as Named>::typehash();
781 let result = resolve_reducer(typehash, None)
782 .unwrap()
783 .unwrap()
784 .reduce_updates(updates)
785 .unwrap()
786 .deserialized::<WatermarkUpdate<T>>()
787 .unwrap();
788
789 for (rank, expected_value) in &expected {
791 assert_eq!(
792 result.get_rank(*rank).unwrap(),
793 expected_value,
794 "Mismatch for rank {rank}"
795 );
796 }
797 assert_eq!(result.num_ranks(), expected.len());
799 }
800
801 verify::<i64>(
802 i64_updates,
803 hashmap! {
804 0 => -9, 1 => -10, 3 => -99, },
808 );
809
810 verify::<u64>(
811 u64_updates,
812 hashmap! {
813 0 => 9, 1 => 10, 3 => 321, },
817 );
818 }
819
820 #[test]
821 fn test_accum_reducer_numeric() {
822 assert_eq!(
823 sum::<u64>().reducer_spec().unwrap().typehash,
824 <SumReducer::<u64> as Named>::typehash(),
825 );
826 assert_eq!(
827 sum::<i64>().reducer_spec().unwrap().typehash,
828 <SumReducer::<i64> as Named>::typehash(),
829 );
830
831 assert_eq!(
832 join_semilattice::<Min<u64>>()
833 .reducer_spec()
834 .unwrap()
835 .typehash,
836 <SemilatticeReducer<Min<u64>> as Named>::typehash(),
837 );
838 assert_eq!(
839 join_semilattice::<Min<i64>>()
840 .reducer_spec()
841 .unwrap()
842 .typehash,
843 <SemilatticeReducer<Min<i64>> as Named>::typehash(),
844 );
845
846 assert_eq!(
847 join_semilattice::<Max<u64>>()
848 .reducer_spec()
849 .unwrap()
850 .typehash,
851 <SemilatticeReducer<Max<u64>> as Named>::typehash(),
852 );
853 assert_eq!(
854 join_semilattice::<Max<i64>>()
855 .reducer_spec()
856 .unwrap()
857 .typehash,
858 <SemilatticeReducer<Max<i64>> as Named>::typehash(),
859 );
860 assert_eq!(
861 unit().reducer_spec().unwrap().typehash,
862 <UnitReducer as Named>::typehash(),
863 );
864 }
865
866 #[test]
867 fn test_comm_reducer_unit() {
868 let unit_updates = serialize(vec![(), (), ()]);
869 let typehash = <UnitReducer as Named>::typehash();
870 assert_eq!(
871 resolve_reducer(typehash, None)
872 .unwrap()
873 .unwrap()
874 .reduce_updates(unit_updates)
875 .unwrap()
876 .deserialized::<()>()
877 .unwrap(),
878 (),
879 );
880 }
881
882 #[test]
883 fn test_accum_reducer_watermark() {
884 fn verify<T: Clone + PartialEq + Named + 'static>() {
885 assert_eq!(
886 join_semilattice::<WatermarkUpdate<T>>()
887 .reducer_spec()
888 .unwrap()
889 .typehash,
890 <SemilatticeReducer<WatermarkUpdate<T>> as Named>::typehash(),
891 );
892 }
893 verify::<u64>();
894 verify::<i64>();
895 }
896
897 #[test]
898 fn test_watermark_accumulator() {
899 let accumulator = join_semilattice::<WatermarkUpdate<u64>>();
900 let ranks_values_expectations = [
901 (0, 1003, 0, 1003),
903 (1, 1002, 1, 1002),
904 (2, 1001, 2, 1001),
905 (0, 100, 3, 100),
907 (1, 101, 4, 100),
908 (2, 102, 5, 100),
909 (0, 100, 6, 100),
911 (1, 101, 7, 100),
912 (2, 102, 8, 100),
913 (0, 1000, 9, 101),
915 (1, 1100, 10, 102),
917 (2, 1200, 11, 1000),
919 (0, 1001, 12, 1001),
921 (1, 1101, 13, 1001),
922 (2, 1201, 14, 1001),
923 (2, 102, 15, 102),
925 (1, 101, 16, 101),
926 (0, 100, 17, 100),
927 ];
928 let mut state = WatermarkUpdate::default();
929 for (rank, value, ts, expected) in ranks_values_expectations {
930 accumulator
931 .accumulate(&mut state, WatermarkUpdate::from((rank, value, ts)))
932 .unwrap();
933 assert_eq!(
934 state.get(),
935 &expected,
936 "rank is {rank}; value is {value}; ts is {ts}"
937 );
938 }
939 }
940
941 #[test]
942 fn test_comm_reducer_gcounter() {
943 let updates = serialize::<GCounterUpdate>(vec![
945 GCounterUpdate::from((0, 10)),
946 GCounterUpdate::from((1, 20)),
947 GCounterUpdate::from((0, 15)), GCounterUpdate::from((2, 5)),
949 GCounterUpdate::from((1, 25)), ]);
951
952 let typehash = <SemilatticeReducer<GCounterUpdate> as Named>::typehash();
953 let result = resolve_reducer(typehash, None)
954 .unwrap()
955 .unwrap()
956 .reduce_updates(updates)
957 .unwrap()
958 .deserialized::<GCounterUpdate>()
959 .unwrap();
960
961 assert_eq!(result.get_rank(0), Some(15));
963 assert_eq!(result.get_rank(1), Some(25));
964 assert_eq!(result.get_rank(2), Some(5));
965 assert_eq!(result.num_ranks(), 3);
966 assert_eq!(result.get(), 45);
968 }
969
970 #[test]
971 fn test_accum_reducer_gcounter() {
972 assert_eq!(
973 join_semilattice::<GCounterUpdate>()
974 .reducer_spec()
975 .unwrap()
976 .typehash,
977 <SemilatticeReducer<GCounterUpdate> as Named>::typehash(),
978 );
979 }
980
981 #[test]
982 fn test_gcounter_accumulator() {
983 let accumulator = join_semilattice::<GCounterUpdate>();
984 let ranks_counts_expectations: [(usize, u64, u64); 17] = [
986 (0, 1000, 1000),
988 (1, 100, 1100),
989 (2, 10, 1110),
990 (2, 20, 1120),
992 (1, 200, 1220),
993 (0, 2000, 2220),
994 (0, 2000, 2220),
996 (1, 200, 2220),
997 (2, 20, 2220),
998 (0, 1, 2220),
1000 (1, 1, 2220),
1001 (2, 1, 2220),
1002 (2, 5000, 7200), (1, 6000, 13000), (0, 10000, 21000), (0, 10001, 21001),
1010 (1, 6001, 21002),
1011 ];
1012 let mut state = GCounterUpdate::default();
1013 for (rank, count, expected) in ranks_counts_expectations {
1014 accumulator
1015 .accumulate(&mut state, GCounterUpdate::from((rank, count)))
1016 .unwrap();
1017 assert_eq!(state.get(), expected, "rank is {rank}; count is {count}");
1018 }
1019 assert_eq!(state.get_rank(0), Some(10001));
1021 assert_eq!(state.get_rank(1), Some(6001));
1022 assert_eq!(state.get_rank(2), Some(5000));
1023 assert_eq!(state.get_rank(3), None);
1024 assert_eq!(state.num_ranks(), 3);
1025 }
1026
1027 #[test]
1028 fn test_gcounter_commutativity() {
1029 let updates = [
1031 GCounterUpdate::from((0, 10)),
1032 GCounterUpdate::from((1, 20)),
1033 GCounterUpdate::from((0, 15)),
1034 GCounterUpdate::from((2, 5)),
1035 GCounterUpdate::from((1, 25)),
1036 ];
1037
1038 let accumulator = join_semilattice::<GCounterUpdate>();
1040 let mut forward = GCounterUpdate::default();
1041 for update in updates.iter().cloned() {
1042 accumulator.accumulate(&mut forward, update).unwrap();
1043 }
1044
1045 let mut reverse = GCounterUpdate::default();
1047 for update in updates.iter().rev().cloned() {
1048 accumulator.accumulate(&mut reverse, update).unwrap();
1049 }
1050
1051 assert_eq!(forward.get(), reverse.get());
1052 assert_eq!(forward.get(), 45); assert_eq!(forward.get_rank(0), reverse.get_rank(0));
1054 assert_eq!(forward.get_rank(1), reverse.get_rank(1));
1055 assert_eq!(forward.get_rank(2), reverse.get_rank(2));
1056 }
1057
1058 #[test]
1059 fn test_comm_reducer_pncounter() {
1060 let updates = serialize::<PNCounterUpdate>(vec![
1062 PNCounterUpdate::inc(0, 10),
1063 PNCounterUpdate::inc(1, 20),
1064 PNCounterUpdate::dec(0, 5),
1065 PNCounterUpdate::inc(0, 15), PNCounterUpdate::dec(1, 8),
1067 PNCounterUpdate::dec(0, 7), ]);
1069
1070 let typehash = <SemilatticeReducer<PNCounterUpdate> as Named>::typehash();
1071 let result = resolve_reducer(typehash, None)
1072 .unwrap()
1073 .unwrap()
1074 .reduce_updates(updates)
1075 .unwrap()
1076 .deserialized::<PNCounterUpdate>()
1077 .unwrap();
1078
1079 assert_eq!(result.get(), 20);
1084 assert_eq!(result.num_inc_ranks(), 2);
1085 assert_eq!(result.num_dec_ranks(), 2);
1086 }
1087
1088 #[test]
1089 fn test_accum_reducer_pncounter() {
1090 assert_eq!(
1091 join_semilattice::<PNCounterUpdate>()
1092 .reducer_spec()
1093 .unwrap()
1094 .typehash,
1095 <SemilatticeReducer<PNCounterUpdate> as Named>::typehash(),
1096 );
1097 }
1098
1099 #[test]
1100 fn test_pncounter_accumulator() {
1101 let accumulator = join_semilattice::<PNCounterUpdate>();
1102 #[derive(Clone, Copy, Debug)]
1104 enum Op {
1105 Inc(usize, u64),
1106 Dec(usize, u64),
1107 }
1108 use Op::*;
1109
1110 let ops_expectations = [
1114 (Inc(0, 100), 100), (Inc(1, 50), 150), (Inc(2, 25), 175), (Dec(0, 10), 165), (Dec(1, 5), 160), (Dec(2, 2), 158), (Inc(0, 200), 258), (Inc(1, 100), 308), (Inc(2, 50), 333), (Dec(0, 20), 323), (Dec(1, 15), 313), (Dec(2, 5), 310), (Inc(0, 200), 310),
1132 (Dec(1, 15), 310),
1133 (Inc(0, 1), 310),
1135 (Dec(0, 1), 310),
1136 (Dec(2, 60), 255), (Dec(1, 120), 150), (Inc(2, 60), 160), (Inc(0, 1000), 960), (Dec(2, 100), 920), ];
1145
1146 let mut state = PNCounterUpdate::default();
1147 for (i, (op, expected)) in ops_expectations.iter().enumerate() {
1148 let update = match op {
1149 Inc(rank, delta) => PNCounterUpdate::inc(*rank, *delta),
1150 Dec(rank, delta) => PNCounterUpdate::dec(*rank, *delta),
1151 };
1152 accumulator.accumulate(&mut state, update).unwrap();
1153 assert_eq!(state.get(), *expected, "step {i}: {op:?}");
1154 }
1155
1156 assert_eq!(state.num_inc_ranks(), 3);
1158 assert_eq!(state.num_dec_ranks(), 3);
1159 }
1160
1161 #[test]
1162 fn test_pncounter_commutativity() {
1163 let updates = [
1165 PNCounterUpdate::inc(0, 10),
1166 PNCounterUpdate::inc(1, 20),
1167 PNCounterUpdate::dec(0, 5),
1168 PNCounterUpdate::inc(0, 15),
1169 PNCounterUpdate::dec(1, 8),
1170 PNCounterUpdate::dec(2, 3),
1171 PNCounterUpdate::inc(2, 12),
1172 ];
1173
1174 let accumulator = join_semilattice::<PNCounterUpdate>();
1176 let mut forward = PNCounterUpdate::default();
1177 for update in updates.iter().cloned() {
1178 accumulator.accumulate(&mut forward, update).unwrap();
1179 }
1180
1181 let mut reverse = PNCounterUpdate::default();
1183 for update in updates.iter().rev().cloned() {
1184 accumulator.accumulate(&mut reverse, update).unwrap();
1185 }
1186
1187 assert_eq!(forward.get(), reverse.get());
1188 assert_eq!(forward.get(), 31); assert_eq!(forward.num_inc_ranks(), reverse.num_inc_ranks());
1190 assert_eq!(forward.num_dec_ranks(), reverse.num_dec_ranks());
1191 }
1192}