nodedb_cluster/distributed_graph/
pattern_match.rs1use std::collections::HashMap;
20
21use serde::{Deserialize, Serialize};
22
23#[derive(
29 Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
30)]
31pub struct PatternContinuation {
32 pub target_shard: u32,
34 pub source_shard: u32,
36 pub bindings: HashMap<String, String>,
38 pub next_triple_idx: usize,
40 pub start_node: String,
42 pub start_binding: String,
44}
45
46#[derive(
48 Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
49)]
50pub struct ShardMatchResult {
51 pub shard_id: u32,
53 pub completed_rows: Vec<HashMap<String, String>>,
55 pub continuations: Vec<PatternContinuation>,
57}
58
59#[derive(Debug)]
64pub struct DistributedMatchCoordinator {
65 pub completed: Vec<HashMap<String, String>>,
67 pub pending: HashMap<u32, Vec<PatternContinuation>>,
69 pub round: u32,
71 pub max_rounds: u32,
73}
74
75pub struct ResolvedContinuationArgs {
77 pub target_shard: u32,
78 pub source_shard: u32,
79 pub bindings: HashMap<String, String>,
80 pub next_triple_idx: usize,
81 pub start_node: String,
82 pub start_binding: String,
83}
84
85impl PatternContinuation {
86 pub fn from_resolved(args: ResolvedContinuationArgs) -> Self {
95 let ResolvedContinuationArgs {
96 target_shard,
97 source_shard,
98 bindings,
99 next_triple_idx,
100 start_node,
101 start_binding,
102 } = args;
103 Self {
104 target_shard,
105 source_shard,
106 bindings,
107 next_triple_idx,
108 start_node,
109 start_binding,
110 }
111 }
112}
113
114impl DistributedMatchCoordinator {
115 pub fn new(max_rounds: u32) -> Self {
116 Self {
117 completed: Vec::new(),
118 pending: HashMap::new(),
119 round: 0,
120 max_rounds,
121 }
122 }
123
124 pub fn add_shard_result(&mut self, result: ShardMatchResult) {
126 self.completed.extend(result.completed_rows);
127 for cont in result.continuations {
128 self.pending
129 .entry(cont.target_shard)
130 .or_default()
131 .push(cont);
132 }
133 }
134
135 pub fn has_pending(&self) -> bool {
137 !self.pending.is_empty()
138 }
139
140 pub fn take_pending(&mut self, shard_id: u32) -> Vec<PatternContinuation> {
142 self.pending.remove(&shard_id).unwrap_or_default()
143 }
144
145 pub fn take_all_pending(&mut self) -> HashMap<u32, Vec<PatternContinuation>> {
147 std::mem::take(&mut self.pending)
148 }
149
150 pub fn advance(&mut self) -> bool {
152 self.round += 1;
153 self.round < self.max_rounds
154 }
155
156 pub fn result_count(&self) -> usize {
158 self.completed.len()
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn pattern_continuation_serde() {
168 let cont = PatternContinuation {
169 target_shard: 2,
170 source_shard: 0,
171 bindings: [("a".into(), "alice".into())].into_iter().collect(),
172 next_triple_idx: 1,
173 start_node: "bob".into(),
174 start_binding: "b".into(),
175 };
176 let bytes = zerompk::to_msgpack_vec(&cont).unwrap();
177 let decoded: PatternContinuation = zerompk::from_msgpack(&bytes).unwrap();
178 assert_eq!(decoded.target_shard, 2);
179 assert_eq!(decoded.start_node, "bob");
180 assert_eq!(decoded.bindings["a"], "alice");
181 }
182
183 #[test]
184 fn shard_match_result_serde() {
185 let result = ShardMatchResult {
186 shard_id: 1,
187 completed_rows: vec![
188 [("a".into(), "alice".into()), ("b".into(), "bob".into())]
189 .into_iter()
190 .collect(),
191 ],
192 continuations: vec![],
193 };
194 let bytes = zerompk::to_msgpack_vec(&result).unwrap();
195 let decoded: ShardMatchResult = zerompk::from_msgpack(&bytes).unwrap();
196 assert_eq!(decoded.completed_rows.len(), 1);
197 }
198
199 #[test]
200 fn coordinator_collects_results() {
201 let mut coord = DistributedMatchCoordinator::new(10);
202
203 coord.add_shard_result(ShardMatchResult {
204 shard_id: 0,
205 completed_rows: vec![[("a".into(), "alice".into())].into_iter().collect()],
206 continuations: vec![PatternContinuation {
207 target_shard: 1,
208 source_shard: 0,
209 bindings: [("a".into(), "alice".into())].into_iter().collect(),
210 next_triple_idx: 1,
211 start_node: "bob".into(),
212 start_binding: "b".into(),
213 }],
214 });
215
216 assert_eq!(coord.result_count(), 1);
217 assert!(coord.has_pending());
218 assert_eq!(coord.take_pending(1).len(), 1);
219 assert!(!coord.has_pending());
220 }
221
222 #[test]
223 fn coordinator_multi_round() {
224 let mut coord = DistributedMatchCoordinator::new(5);
225
226 coord.add_shard_result(ShardMatchResult {
228 shard_id: 0,
229 completed_rows: vec![
230 [("x".into(), "1".into())].into_iter().collect(),
231 [("x".into(), "2".into())].into_iter().collect(),
232 ],
233 continuations: vec![PatternContinuation {
234 target_shard: 1,
235 source_shard: 0,
236 bindings: HashMap::new(),
237 next_triple_idx: 0,
238 start_node: "n".into(),
239 start_binding: "a".into(),
240 }],
241 });
242
243 assert!(coord.advance()); let pending = coord.take_all_pending();
247 assert_eq!(pending.len(), 1);
248 assert_eq!(pending[&1].len(), 1);
249
250 coord.add_shard_result(ShardMatchResult {
251 shard_id: 1,
252 completed_rows: vec![[("x".into(), "3".into())].into_iter().collect()],
253 continuations: vec![],
254 });
255
256 assert!(!coord.has_pending());
257 assert_eq!(coord.result_count(), 3);
258 }
259
260 #[test]
261 fn coordinator_max_rounds() {
262 let mut coord = DistributedMatchCoordinator::new(2);
263 assert!(coord.advance()); assert!(!coord.advance()); }
266
267 #[test]
268 fn coordinator_no_pending_initially() {
269 let coord = DistributedMatchCoordinator::new(10);
270 assert!(!coord.has_pending());
271 assert_eq!(coord.result_count(), 0);
272 }
273}