hashiverse_lib/client/timeline/
recursive_bucket_visitor.rs1use crate::tools::buckets::BucketLocation;
25use crate::tools::time::{DurationMillis, TimeMillis};
26
27pub enum RecursiveBucketVisitorOpenCallbackResult<T> {
28 ContinueWithChildren(T),
29 ContinueWithoutChildren(T),
30 Stop,
31}
32
33pub enum RecursiveBucketVisitorCloseCallbackResult {
34 Continue,
35 Stop,
36}
37
38pub struct RecursiveBucketVisitor {}
39
40impl RecursiveBucketVisitor {
41 pub async fn visit<T, FOpen, FClose>(time_millis_max_exclusive: TimeMillis, time_millis_min: TimeMillis, bucket_durations: &[DurationMillis], on_bucket_open: &mut FOpen, on_bucket_close: &mut FClose) -> anyhow::Result<bool>
42 where
43 FOpen: AsyncFnMut(TimeMillis, DurationMillis) -> anyhow::Result<RecursiveBucketVisitorOpenCallbackResult<T>>,
44 FClose: AsyncFnMut(TimeMillis, DurationMillis, T) -> anyhow::Result<RecursiveBucketVisitorCloseCallbackResult>,
45 {
46 struct FrameOpen {
47 bucket_time_millis: TimeMillis,
48 bucket_duration_i: usize,
49 oldest_bucket_time_millis_allowed: TimeMillis,
50 }
51
52 struct FrameClose<T> {
53 bucket_time_millis: TimeMillis,
54 bucket_duration_i: usize,
55 payload: T,
56 }
57
58 enum Frame<T> {
59 Open(FrameOpen),
60 Close(FrameClose<T>),
61 }
62
63 anyhow::ensure!(!bucket_durations.is_empty(), "bucket_durations must have at least one element");
64
65 let mut frames = Vec::new();
66
67 {
69 let next_bucket_duration_i = 0;
70 let next_bucket_duration = bucket_durations[next_bucket_duration_i];
71
72 let next_latest_bucket_time_millis = BucketLocation::round_down_to_bucket_start(time_millis_max_exclusive, next_bucket_duration);
73 let next_oldest_bucket_time_millis_allowed = BucketLocation::round_down_to_bucket_start(time_millis_min, next_bucket_duration);
74
75 frames.push(Frame::Open(FrameOpen {
76 bucket_time_millis: next_latest_bucket_time_millis,
77 bucket_duration_i: next_bucket_duration_i,
78 oldest_bucket_time_millis_allowed: next_oldest_bucket_time_millis_allowed,
79 }));
80 }
81
82 while let Some(frame) = frames.pop() {
84 match frame {
85 Frame::Close(FrameClose { bucket_time_millis, bucket_duration_i, payload }) => {
86 let bucket_duration = bucket_durations[bucket_duration_i];
87 let callback_result = on_bucket_close(bucket_time_millis, bucket_duration, payload).await?;
88 if let RecursiveBucketVisitorCloseCallbackResult::Stop = callback_result {
89 return Ok(false);
90 };
91 }
92
93 Frame::Open(FrameOpen {
94 bucket_time_millis,
95 bucket_duration_i,
96 oldest_bucket_time_millis_allowed,
97 }) => {
98 let bucket_duration = bucket_durations[bucket_duration_i];
99
100 {
102 let next_bucket_duration_i = bucket_duration_i;
103 let next_bucket_time_millis = bucket_time_millis - bucket_duration;
104 let next_oldest_bucket_time_millis_allowed = oldest_bucket_time_millis_allowed;
105
106 if next_bucket_time_millis >= oldest_bucket_time_millis_allowed {
107 frames.push(Frame::Open(FrameOpen {
108 bucket_time_millis: next_bucket_time_millis,
109 bucket_duration_i: next_bucket_duration_i,
110 oldest_bucket_time_millis_allowed: next_oldest_bucket_time_millis_allowed,
111 }));
112 }
113 }
114
115 if bucket_time_millis >= time_millis_max_exclusive {
117 continue;
118 }
119
120 let (payload, continue_with_children) = match on_bucket_open(bucket_time_millis, bucket_duration).await? {
121 RecursiveBucketVisitorOpenCallbackResult::ContinueWithChildren(payload) => (payload, true),
122 RecursiveBucketVisitorOpenCallbackResult::ContinueWithoutChildren(payload) => (payload, false),
123 RecursiveBucketVisitorOpenCallbackResult::Stop => return Ok(false),
125 };
126
127 frames.push(Frame::Close(FrameClose { bucket_time_millis, bucket_duration_i, payload }));
129
130 if continue_with_children {
132 let next_bucket_duration_i = bucket_duration_i + 1;
133 if next_bucket_duration_i < bucket_durations.len() {
134 let next_bucket_duration = bucket_durations[next_bucket_duration_i];
135 let time_millis_max_bucket = BucketLocation::round_down_to_bucket_start(time_millis_max_exclusive, next_bucket_duration);
136
137 let next_bucket_time_millis = time_millis_max_bucket.min(bucket_time_millis + bucket_duration - next_bucket_duration);
138 let next_oldest_bucket_time_millis_allowed = bucket_time_millis;
139
140 frames.push(Frame::Open(FrameOpen {
141 bucket_time_millis: next_bucket_time_millis,
142 bucket_duration_i: next_bucket_duration_i,
143 oldest_bucket_time_millis_allowed: next_oldest_bucket_time_millis_allowed,
144 }));
145 }
146 }
147 }
148 }
149 }
150
151 Ok(true)
152 }
153}
154
155#[cfg(test)]
156pub mod tests {
157 use crate::client::timeline::recursive_bucket_visitor::{RecursiveBucketVisitor, RecursiveBucketVisitorCloseCallbackResult, RecursiveBucketVisitorOpenCallbackResult};
158 use crate::tools::buckets::BUCKET_DURATIONS;
159 use crate::tools::time::{DurationMillis, MILLIS_IN_DAY, MILLIS_IN_MILLISECOND, MILLIS_IN_MONTH, MILLIS_IN_WEEK, TimeMillis};
160 use log::info;
161
162 #[tokio::test]
163 async fn visitor_test() -> anyhow::Result<()> {
164 {
167 info!("Just at a new month (so only one month in view)");
168 let mut total_buckets = 0;
169
170 RecursiveBucketVisitor::visit(
171 TimeMillis::zero() + MILLIS_IN_MONTH,
172 TimeMillis::zero(),
173 &BUCKET_DURATIONS[1..3],
174 &mut async |time_millis, duration_millis| {
175 info!("time_millis: {}, duration_millis: {}", time_millis, duration_millis);
176 total_buckets += 1;
177 Ok(RecursiveBucketVisitorOpenCallbackResult::ContinueWithChildren(()))
178 },
179 &mut async |_time_millis: TimeMillis, _duration_millis: DurationMillis, _payload: ()| Ok(RecursiveBucketVisitorCloseCallbackResult::Continue),
180 )
181 .await?;
182
183 assert_eq!(total_buckets, 5);
184 }
185
186 {
187 info!("Just after a new month (one entire month and the beginning of one month in view)");
188 let mut total_buckets = 0;
189
190 RecursiveBucketVisitor::visit(
191 TimeMillis::zero() + MILLIS_IN_MONTH + MILLIS_IN_MILLISECOND,
192 TimeMillis::zero(),
193 &BUCKET_DURATIONS[1..3],
194 &mut async |time_millis: TimeMillis, duration_millis: DurationMillis| {
195 info!("time_millis: {}, duration_millis: {}", time_millis, duration_millis);
196 total_buckets += 1;
197 Ok(RecursiveBucketVisitorOpenCallbackResult::ContinueWithChildren(()))
198 },
199 &mut async |_time_millis: TimeMillis, _duration_millis: DurationMillis, _payload: ()| Ok(RecursiveBucketVisitorCloseCallbackResult::Continue),
200 )
201 .await?;
202
203 assert_eq!(total_buckets, 7);
204 }
205
206 {
207 info!("Just at a new month plus a 2 weeks plus 3 days");
208 let mut total_buckets = 0;
209
210 RecursiveBucketVisitor::visit(
211 TimeMillis::zero() + MILLIS_IN_MONTH + MILLIS_IN_WEEK.const_mul(2) + MILLIS_IN_DAY.const_mul(3),
212 TimeMillis::zero(),
213 &BUCKET_DURATIONS[1..4],
214 &mut async |time_millis, duration_millis| {
215 info!("time_millis: {}, duration_millis: {}", time_millis, duration_millis);
216 total_buckets += 1;
217 Ok(RecursiveBucketVisitorOpenCallbackResult::ContinueWithChildren(()))
218 },
219 &mut async |_time_millis: TimeMillis, _duration_millis: DurationMillis, _payload: ()| Ok(RecursiveBucketVisitorCloseCallbackResult::Continue),
220 )
221 .await?;
222
223 assert_eq!(total_buckets, 54);
224 }
225
226 Ok(())
227 }
228
229 #[tokio::test]
230 async fn visitor_granular_test() -> anyhow::Result<()> {
231 {
234 info!("Just before the beginning of a new month (so only one month in view) - but mega granular");
235 let mut total_buckets = 0;
236
237 RecursiveBucketVisitor::visit(
238 TimeMillis::zero() + MILLIS_IN_MONTH,
239 TimeMillis::zero(),
240 &BUCKET_DURATIONS[1..],
241 &mut async |time_millis: TimeMillis, duration_millis: DurationMillis| {
242 info!("time_millis: {}, duration_millis: {}", time_millis, duration_millis);
243 total_buckets += 1;
244 Ok(RecursiveBucketVisitorOpenCallbackResult::ContinueWithChildren(()))
245 },
246 &mut async |_time_millis: TimeMillis, _duration_millis: DurationMillis, _payload: ()| Ok(RecursiveBucketVisitorCloseCallbackResult::Continue),
247 )
248 .await?;
249
250 let total_expected = 1 + 4 * (1 + 7 * (1 + 4 * (1 + 6 * (1 + 4 * (1 + 3 * (1 + 5))))));
251 assert_eq!(total_buckets, total_expected);
252 }
253
254 Ok(())
255 }
256
257 #[tokio::test]
258 async fn visitor_granular_with_recurse_sibling_test() -> anyhow::Result<()> {
259 {
262 info!("Limit the depth we want to go based on a condition");
263 let mut total_buckets = 0;
264
265 RecursiveBucketVisitor::visit(
266 TimeMillis::zero() + MILLIS_IN_MONTH,
267 TimeMillis::zero(),
268 &BUCKET_DURATIONS[1..],
269 &mut async |time_millis: TimeMillis, duration_millis: DurationMillis| {
270 info!("time_millis: {}, duration_millis: {}", time_millis, duration_millis);
271 total_buckets += 1;
272
273 if duration_millis > MILLIS_IN_DAY {
274 Ok(RecursiveBucketVisitorOpenCallbackResult::ContinueWithChildren(()))
275 }
276 else {
277 Ok(RecursiveBucketVisitorOpenCallbackResult::ContinueWithoutChildren(()))
278 }
279 },
280 &mut async |_time_millis: TimeMillis, _duration_millis: DurationMillis, _payload: ()| Ok(RecursiveBucketVisitorCloseCallbackResult::Continue),
281 )
282 .await?;
283
284 let total_expected = 1 + 4 * (1 + 7);
285 assert_eq!(total_buckets, total_expected);
286 }
287
288 Ok(())
289 }
290
291 #[tokio::test]
292 async fn visitor_granular_with_abort_test() -> anyhow::Result<()> {
293 {
296 info!("Abort after a few visits");
297 let mut total_buckets = 0;
298
299 RecursiveBucketVisitor::visit(
300 TimeMillis::zero() + MILLIS_IN_MONTH,
301 TimeMillis::zero(),
302 &BUCKET_DURATIONS[1..],
303 &mut async |bucket_time_millis: TimeMillis, bucket_duration_millis: DurationMillis| {
304 info!("bucket_time_millis: {}, duration_millis: {}", bucket_time_millis, bucket_duration_millis);
305 total_buckets += 1;
306
307 if total_buckets >= 9 {
308 Ok(RecursiveBucketVisitorOpenCallbackResult::Stop)
309 }
310 else {
311 Ok(RecursiveBucketVisitorOpenCallbackResult::ContinueWithChildren(()))
312 }
313 },
314 &mut async |_time_millis: TimeMillis, _duration_millis: DurationMillis, _payload: ()| Ok(RecursiveBucketVisitorCloseCallbackResult::Continue),
315 )
316 .await?;
317
318 assert_eq!(total_buckets, 9);
319 }
320
321 Ok(())
322 }
323
324 #[tokio::test]
325 async fn visitor_delivers_open_payload_to_matching_close() -> anyhow::Result<()> {
326 let mut total_opens = 0;
327 let mut total_closes = 0;
328
329 RecursiveBucketVisitor::visit(
330 TimeMillis::zero() + MILLIS_IN_MONTH + MILLIS_IN_WEEK.const_mul(2) + MILLIS_IN_DAY.const_mul(3),
331 TimeMillis::zero(),
332 &BUCKET_DURATIONS[1..4],
333 &mut async |bucket_time_millis: TimeMillis, bucket_duration_millis: DurationMillis| {
334 total_opens += 1;
335 Ok(RecursiveBucketVisitorOpenCallbackResult::ContinueWithChildren((bucket_time_millis, bucket_duration_millis)))
336 },
337 &mut async |bucket_time_millis: TimeMillis, bucket_duration_millis: DurationMillis, payload: (TimeMillis, DurationMillis)| {
338 total_closes += 1;
339 assert_eq!(payload, (bucket_time_millis, bucket_duration_millis), "a close must receive the payload of its own open");
340 Ok(RecursiveBucketVisitorCloseCallbackResult::Continue)
341 },
342 )
343 .await?;
344
345 assert_eq!(total_opens, 54);
346 assert_eq!(total_closes, total_opens);
347
348 Ok(())
349 }
350}