1use std::collections::HashMap;
2use std::error::Error;
3use std::hash::Hash;
4use std::marker::PhantomData;
5use std::sync::Arc;
6use tokio::task::JoinSet;
7
8pub struct MapReduce<K, V, R>
10where
11 K: Clone + Eq + Hash + Send + 'static,
12 V: Send + 'static,
13 R: Send + 'static,
14{
15 max_concurrent_tasks: usize,
16 _marker: PhantomData<(K, V, R)>,
17}
18
19impl<K, V, R> MapReduce<K, V, R>
20where
21 K: Clone + Eq + Hash + Send + 'static,
22 V: Send + 'static,
23 R: Send + 'static,
24{
25 pub fn new(max_concurrent_tasks: usize) -> Self {
27 assert!(
28 max_concurrent_tasks > 0,
29 "maximum concurrent tasks must be greater than zero"
30 );
31
32 Self {
33 max_concurrent_tasks,
34 _marker: PhantomData,
35 }
36 }
37
38 pub async fn execute<M, Red, I>(
40 &self,
41 input_data: I,
42 map_fn: M,
43 reduce_fn: Red,
44 ) -> Result<HashMap<K, R>, Box<dyn Error + Send + Sync>>
45 where
46 I: IntoIterator<Item = V> + Send + 'static,
47 M: Fn(V) -> Vec<(K, R)> + Send + Sync + 'static,
48 Red: Fn(K, Vec<R>) -> R + Send + Sync + 'static,
49 {
50 let map_results = self.map_phase(input_data, map_fn).await?;
51 let grouped_results = self.shuffle_phase(map_results);
52 self.reduce_phase(grouped_results, reduce_fn).await
53 }
54
55 async fn map_phase<M, I>(
56 &self,
57 input_data: I,
58 map_fn: M,
59 ) -> Result<Vec<(K, R)>, Box<dyn Error + Send + Sync>>
60 where
61 I: IntoIterator<Item = V> + Send + 'static,
62 M: Fn(V) -> Vec<(K, R)> + Send + Sync + 'static,
63 {
64 let mut jobs = JoinSet::new();
65 let mut mapped = Vec::new();
66 let map_fn = Arc::new(map_fn);
67
68 for item in input_data {
69 if jobs.len() == self.max_concurrent_tasks {
70 mapped.extend(
71 jobs.join_next()
72 .await
73 .expect("a non-empty task set must yield a task")?,
74 );
75 }
76
77 let map_fn = Arc::clone(&map_fn);
78 jobs.spawn(async move { map_fn(item) });
79 }
80
81 while let Some(result) = jobs.join_next().await {
82 mapped.extend(result?);
83 }
84
85 Ok(mapped)
86 }
87
88 fn shuffle_phase(&self, map_results: Vec<(K, R)>) -> HashMap<K, Vec<R>> {
89 let mut grouped: HashMap<K, Vec<R>> = HashMap::new();
90
91 for (key, value) in map_results {
92 grouped.entry(key).or_default().push(value);
93 }
94
95 grouped
96 }
97
98 async fn reduce_phase<Red>(
99 &self,
100 grouped_results: HashMap<K, Vec<R>>,
101 reduce_fn: Red,
102 ) -> Result<HashMap<K, R>, Box<dyn Error + Send + Sync>>
103 where
104 Red: Fn(K, Vec<R>) -> R + Send + Sync + 'static,
105 {
106 let mut jobs = JoinSet::new();
107 let mut reduced = HashMap::new();
108 let reduce_fn = Arc::new(reduce_fn);
109
110 for (key, values) in grouped_results {
111 if jobs.len() == self.max_concurrent_tasks {
112 let (key, value) = jobs
113 .join_next()
114 .await
115 .expect("a non-empty task set must yield a task")?;
116 reduced.insert(key, value);
117 }
118
119 let reduce_fn = Arc::clone(&reduce_fn);
120 jobs.spawn(async move {
121 let value = reduce_fn(key.clone(), values);
122 (key, value)
123 });
124 }
125
126 while let Some(result) = jobs.join_next().await {
127 let (key, value) = result?;
128 reduced.insert(key, value);
129 }
130
131 Ok(reduced)
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::MapReduce;
138 use std::sync::atomic::{AtomicUsize, Ordering};
139 use std::sync::Arc;
140 use std::time::Duration;
141
142 #[tokio::test]
143 async fn counts_words_across_documents() {
144 let map_reduce = MapReduce::<String, String, i32>::new(2);
145 let documents = vec!["the quick brown fox".to_owned(), "the quick dog".to_owned()];
146
147 let results = map_reduce
148 .execute(
149 documents,
150 |document| {
151 document
152 .split_whitespace()
153 .map(|word| (word.to_owned(), 1))
154 .collect()
155 },
156 |_, counts| counts.into_iter().sum(),
157 )
158 .await
159 .unwrap();
160
161 assert_eq!(results.get("the"), Some(&2));
162 assert_eq!(results.get("quick"), Some(&2));
163 assert_eq!(results.get("brown"), Some(&1));
164 assert_eq!(results.get("dog"), Some(&1));
165 }
166
167 #[tokio::test]
168 async fn processes_more_than_channel_capacity_without_deadlocking() {
169 let map_reduce = MapReduce::<usize, usize, usize>::new(1);
170 let results = tokio::time::timeout(
171 Duration::from_secs(1),
172 map_reduce.execute(
173 0..1_000,
174 |value| vec![(value, value)],
175 |_, values| values[0],
176 ),
177 )
178 .await
179 .expect("map-reduce must not deadlock");
180
181 assert_eq!(results.unwrap().len(), 1_000);
182 }
183
184 #[tokio::test]
185 async fn respects_the_configured_concurrency_limit() {
186 let map_reduce = MapReduce::<usize, usize, usize>::new(3);
187 let active = Arc::new(AtomicUsize::new(0));
188 let maximum = Arc::new(AtomicUsize::new(0));
189
190 let results = map_reduce
191 .execute(
192 0..24,
193 {
194 let active = Arc::clone(&active);
195 let maximum = Arc::clone(&maximum);
196 move |value| {
197 let current = active.fetch_add(1, Ordering::SeqCst) + 1;
198 maximum.fetch_max(current, Ordering::SeqCst);
199 std::thread::sleep(Duration::from_millis(5));
200 active.fetch_sub(1, Ordering::SeqCst);
201 vec![(value, value)]
202 }
203 },
204 |_, values| values[0],
205 )
206 .await
207 .unwrap();
208
209 assert_eq!(results.len(), 24);
210 assert!(maximum.load(Ordering::SeqCst) <= 3);
211 }
212
213 #[test]
214 #[should_panic(expected = "maximum concurrent tasks must be greater than zero")]
215 fn rejects_a_zero_concurrency_limit() {
216 MapReduce::<usize, usize, usize>::new(0);
217 }
218}