mpmc_resize/resize.rs
1use alloc::boxed::Box;
2use core::{marker::PhantomData, ptr::null_mut};
3
4use crossbeam_utils::CachePadded;
5
6use crate::{
7 BoundedCollection,
8 sync::atomic::{AtomicBool, AtomicPtr, AtomicUsize, Ordering},
9 utils::Backoff,
10};
11
12/// A dynamically resizable wrapper around a [`BoundedCollection`].
13///
14/// This type implements an algorithm that allows any [`BoundedCollection`] to be dynamically resized while preserving some of its properties.
15///
16/// ## Progress Guarantees:
17///
18/// - **Lock Freedom**: if the wrapped collection is lock-free, all corresponding operations on `Resizable` are also lock-free.
19/// - **Obstruction Freedom**: if the wrapped collection exposes obstruction-free methods, all corresponding operations on `Resizable` are also obstruction-free.
20///
21/// `Resizable::resize` is blocking both on allocator and stale readers and writers.
22///
23/// ## Ordering and Consistency Guarantees:
24///
25/// - **Relaxed FIFO**: if the wrapped collection has FIFO ordering, `Resizable` has **k-FIFO** ordering with asymmetric delay and rank error.
26/// - **Linearizability**: if the wrapped collection is linearizable, all operations on `Resizable` are also linearizable with respect to its relaxed FIFO specification.
27/// - **Empty-Linearizability**: if the wrapped collection is linearizale, all operations on [`Resizable`] are empty-linearizable with respect to the wrapped collections specification.
28///
29/// If no call to `resize` happens, or in steady-state, `Resizable` has strict FIFO ordering and is strictly linearizable, given the same holds for the wrapped collection.
30///
31/// For more intormation on rank error and delay consult the crate level documentation.
32#[derive(Debug)]
33pub struct Resizable<Q> {
34 cores: [AtomicPtr<Q>; 2],
35 push_epoch: CachePadded<AtomicUsize>,
36 pop_epoch: CachePadded<AtomicUsize>,
37 active_pushes: CachePadded<[AtomicUsize; 2]>,
38 active_reads: CachePadded<[AtomicUsize; 2]>,
39 is_resizing: AtomicBool,
40 _marker: PhantomData<Box<Q>>,
41}
42
43impl<Q> Resizable<Q> {
44 /// Constructs a new `Resizable` from two raw [`BoundedCollection`] objects.
45 pub fn from_parts(left: Q, right: Q) -> Self {
46 Self {
47 cores: [
48 AtomicPtr::new(Box::into_raw(Box::new(left))),
49 AtomicPtr::new(Box::into_raw(Box::new(right))),
50 ],
51 active_pushes: [AtomicUsize::new(0), AtomicUsize::new(0)].into(),
52 active_reads: [AtomicUsize::new(0), AtomicUsize::new(0)].into(),
53 push_epoch: AtomicUsize::new(0).into(),
54 pop_epoch: AtomicUsize::new(0).into(),
55 is_resizing: AtomicBool::new(false),
56 _marker: PhantomData,
57 }
58 }
59}
60
61impl<Q> Resizable<Q>
62where
63 Q: BoundedCollection,
64{
65 /// Constructs a new `Resizable` with capacity `size`.
66 #[track_caller]
67 pub fn with_capacity(size: usize) -> Resizable<Q> {
68 Self::from_parts(Q::with_capacity(size), Q::with_capacity(1))
69 }
70
71 /// Constructs a new `Resizable`
72 #[track_caller]
73 pub fn new() -> Self {
74 Self::from_parts(Q::with_capacity(1), Q::with_capacity(1))
75 }
76}
77
78impl<Q> Default for Resizable<Q>
79where
80 Q: Default,
81{
82 fn default() -> Self {
83 Self::from_parts(Q::default(), Q::default())
84 }
85}
86
87impl<Q> Drop for Resizable<Q> {
88 fn drop(&mut self) {
89 let left = self.cores[0].swap(null_mut(), Ordering::Acquire);
90 if !left.is_null() {
91 // Safety:
92 // No concurrent drops of this ds can happen.
93 // This queue was allocated in `new` or in `grow_by` with `Box::into_raw` and was not deallocated since then.
94 // we just checked that its non-null
95 _ = unsafe { Box::from_raw(left) };
96 }
97 let right = self.cores[1].swap(null_mut(), Ordering::Acquire);
98 if !right.is_null() {
99 // Safety:
100 // No concurrent drops of this ds can happen.
101 // This queue was allocated in `new` or in `grow_by` with `Box::into_raw` and was not deallocated since then.
102 // we just checked that its non-null
103 _ = unsafe { Box::from_raw(right) };
104 }
105 }
106}
107
108// we need SeqCst on epoch atomics and active_* atomics on the synchronization points,
109// because we have to synchronize between an asymmetric read/write - write/read pattern across the two
110
111impl<Q> Resizable<Q>
112where
113 Q: BoundedCollection,
114{
115 /// Attempts to resize the capacity of the collection to `size` slots.
116 ///
117 /// **Note:** This method may block (on the allocator) or fail spuriously.
118 /// Further a growth event may not be considered finished in regards of an other `resize` being possible until some time after the call to `resize`.
119 ///
120 /// Returns `true` if the resize was successfull, or `false` if
121 /// it failed. Failure can occur due to thread
122 /// contention, incomplete migration of the previous resize, i.e. staleness of the datastructure,
123 /// or other implementation-specific/spurious conditions.
124 pub fn resize(&self, size: usize) -> bool {
125 if size == 0 {
126 return false;
127 }
128 #[cfg(loom)]
129 crate::sync::atomic::fence(Ordering::SeqCst);
130 let push_epoch = self.push_epoch.load(Ordering::SeqCst);
131 let pop_epoch = self.pop_epoch.load(Ordering::SeqCst);
132
133 if pop_epoch != push_epoch {
134 return false;
135 }
136
137 let old_idx = (push_epoch + 1) % 2;
138
139 // wait on any stale readers of the old queue.
140 // Note that at any point during AND after this check other threads may still register as NEW readers on the OLD queue;
141 // This is safe, because they will revalidate the epoch after registration and NOT actually read the underlying queue.
142 // However this means that we may spuriously fail for longer than strictly necessary to ensure noone is actually reading the old queue.
143 if self.active_reads[old_idx].load(Ordering::Acquire) != 0
144 || self.active_pushes[old_idx].load(Ordering::Acquire) != 0
145 {
146 return false;
147 }
148
149 if self.is_resizing.swap(true, Ordering::AcqRel) {
150 return false;
151 }
152
153 if self.push_epoch.load(Ordering::Acquire) != push_epoch {
154 // could happen if an entire resize happens between load and this check
155 self.is_resizing.store(false, Ordering::Release);
156 return false;
157 }
158
159 // at this point we know that
160 // a) no concurrent resize is happening
161 // b) since pop_epoch == push_epoch the old queue is empty.
162 // c) since (b) and active_* were both 0 at some point t, all conccurrent ops that are registered on active_*[old_idx] will now revalidate their epoch before accessing the queue
163
164 let new_queue = Box::into_raw(Box::new(Q::with_capacity(size)));
165
166 // Safety:
167 // since pop_epoch == push_epoch all concurrent threads acces the queue at push_epoch % 2.
168 // pop ensures that no pushes are in flight to the old queue anymore and that it is empty. We can safely drop it.
169 let old_queue = self.cores[old_idx].swap(new_queue, Ordering::AcqRel);
170
171 self.push_epoch.fetch_add(1, Ordering::Release);
172
173 // Safety:
174 // old_queue was ocnstucted from a Box::into_raw and is dropped only once, as ensured by epoch guards
175 let q = unsafe { Box::from_raw(old_queue) };
176
177 #[cfg(not(any(loom, shuttle)))]
178 debug_assert!(q.try_pop().is_none());
179 #[cfg(any(loom, shuttle))]
180 assert!(q.try_pop().is_none());
181
182 self.is_resizing.store(false, Ordering::Release);
183 true
184 }
185}
186
187impl<Q> Resizable<Q> {
188 fn get_queue(&self, epoch: usize) -> &Q {
189 let queue = self.cores[epoch % 2].load(Ordering::Acquire);
190 // Safety:
191 // It is guranteed by `resize` that no concurrent mutable access can happen to any queue in cores.
192 // It is safe to access it concurrently via shared ref, as long as queue core is Sync.
193 unsafe { &*queue }
194 }
195
196 fn register_push(&self, target_epoch: usize) -> bool {
197 #[cfg(loom)]
198 crate::sync::atomic::fence(Ordering::SeqCst);
199 self.active_reads[target_epoch % 2].fetch_add(1, Ordering::SeqCst);
200
201 #[cfg(loom)]
202 crate::sync::atomic::fence(Ordering::SeqCst);
203 let current_push = self.push_epoch.load(Ordering::SeqCst);
204
205 // It is safe to read if the target epoch is still structurally active
206 if target_epoch != current_push {
207 self.deregister_reader(target_epoch);
208 return false;
209 }
210 true
211 }
212
213 fn register_pop(&self, target_epoch: usize) -> bool {
214 #[cfg(loom)]
215 crate::sync::atomic::fence(Ordering::SeqCst);
216 self.active_reads[target_epoch % 2].fetch_add(1, Ordering::SeqCst);
217
218 #[cfg(loom)]
219 crate::sync::atomic::fence(Ordering::SeqCst);
220 let current_pop = self.pop_epoch.load(Ordering::SeqCst);
221
222 // It is safe to read if the target epoch is still structurally active
223 if target_epoch != current_pop {
224 self.deregister_reader(target_epoch);
225 return false;
226 }
227 true
228 }
229
230 fn deregister_reader(&self, epoch: usize) {
231 self.active_reads[epoch % 2].fetch_sub(1, Ordering::Release);
232 }
233}
234
235impl<Q> Resizable<Q>
236where
237 Q: BoundedCollection,
238{
239 fn try_pop_from(
240 &self,
241 epoch: usize,
242 registration: impl Fn(&Self, usize) -> bool,
243 ) -> Result<Option<Q::Item>, ()> {
244 if !registration(self, epoch) {
245 return Err(());
246 }
247
248 let item = self.get_queue(epoch).try_pop();
249
250 self.deregister_reader(epoch);
251
252 Ok(item)
253 }
254}
255
256impl<Q> BoundedCollection for Resizable<Q>
257where
258 Q: BoundedCollection,
259{
260 type Item = Q::Item;
261
262 fn try_push(&self, item: Self::Item) -> Result<(), Self::Item> {
263 let mut backoff = Backoff::new();
264 loop {
265 let push_epoch = self.push_epoch.load(Ordering::Acquire);
266 self.active_pushes[push_epoch % 2].fetch_add(1, Ordering::SeqCst);
267
268 #[cfg(loom)]
269 crate::sync::atomic::fence(Ordering::SeqCst);
270 if self.push_epoch.load(Ordering::SeqCst) == push_epoch {
271 let r = self.get_queue(push_epoch).try_push(item);
272
273 self.active_pushes[push_epoch % 2].fetch_sub(1, Ordering::Release);
274 return r;
275 }
276 self.active_pushes[push_epoch % 2].fetch_sub(1, Ordering::Release);
277 backoff.backoff();
278 }
279 }
280
281 fn try_pop(&self) -> Option<Self::Item> {
282 #[cfg(any(shuttle, loom))]
283 let mut backoff = Backoff::new();
284
285 // if push_epoch != pop_epoch, we need to drain the old queue.
286 // In order to provide `empty-linearizability` we do a double collect over BOTH queues.
287 //
288 // If push_epoch == pop_epoch we only need to do another sweep IF push_epoch has changed by the end of this call
289 for _ in 0..2 {
290 loop {
291 let push_epoch = self.push_epoch.load(Ordering::Acquire);
292 let pop_epoch = self.pop_epoch.load(Ordering::Acquire);
293
294 if pop_epoch != push_epoch {
295 // drain old buffer
296
297 // it is safe to call get_queue on pop_epoch here, since no resize can happen while we have not updated pop_epoch and reads on this epoch are happening
298 let Ok(item) = self.try_pop_from(pop_epoch, Self::register_pop) else {
299 #[cfg(any(shuttle, loom))]
300 backoff.backoff();
301 continue;
302 };
303
304 if item.is_some() {
305 return item;
306 }
307
308 if self.active_pushes[pop_epoch % 2].load(Ordering::Acquire) == 0 {
309 let Ok(item) = self.try_pop_from(pop_epoch, Self::register_pop) else {
310 #[cfg(any(shuttle, loom))]
311 backoff.backoff();
312 continue;
313 };
314
315 if item.is_some() {
316 return item;
317 }
318
319 _ = self.pop_epoch.compare_exchange_weak(
320 pop_epoch,
321 pop_epoch + 1,
322 Ordering::AcqRel,
323 Ordering::Relaxed,
324 );
325
326 #[cfg(any(shuttle, loom))]
327 backoff.backoff();
328 continue;
329 }
330
331 // at this point the old queue did not contain any items, even though items are in-flight. At this point the new container may already contain items.
332 // We face a tradeoff:
333 //
334 // a) continue in the inner loop and block on the active_pushers -> violates non-blocking/obstruction-freedom guarantees
335 // b) check the new container -> opens up the possibilty for item reordering, even in spsc scenarios, i.e. violates FIFO guarantees + linearizability
336 // It is worth noting here that the extend of reordering per item (i.e. the rank error) is bounded exactly by the number of threads concurrently executing `push`
337 // and the number of items reordered (i.e. the delay) is bounded by exactly those threads calling `pop` in this scenario.
338 // In practice the rank error and delay are much lower, because a specific schedule is reuqired for a reordering to happen.
339 // c) bail, even though the container is non-empty -> violates linearizability (and emptiness assumptions) in that case.
340 // Even worse: if some active_pusher is indefinitely dead, we will henceforth only bail. Thus this option implicitly blocks on the stalled pusher.
341 //
342 // c is of course unaccaptable.
343 //
344 // This would be circumventeable iff a helping mechanims where added or the old container were inactivated,
345 // both of which is not possible given the opaque inner container type and without large changes to the algorithm.
346 //
347 // the fundamental question is:
348 // Do we want complete lock-freedom in `push` and `pop`, or do we want strict 0-FIFO ordering + linearizability always.
349 // This tradeoff may fall differently in different contexts, however it seems reasonable to relax strict FIFO guarantees and linearizability
350 // and accept N-FIFO semantics while resizing the queue. N-FIFO semantics should in practice keep most of the benefits of 0-FIFO, while still preserving lock-freedom.
351 // Note that we do still preserve `empty-linearizability` here by double collecting:
352 // if the first iteration turns out to be double None, we have two possibilities:
353 // a) the old queue was truly empty at the point of popping from the new queue
354 // b) the old queue was non-empty at that point
355 // if the second iteration returns an item, this doesnt matter
356 // if it returns None also (for the old queue), then we have know that a was correct OR the item was popped by someone else, in which case we are also safe.
357 }
358
359 let Ok(item) = self.try_pop_from(push_epoch, Self::register_push) else {
360 #[cfg(any(shuttle, loom))]
361 backoff.backoff();
362 continue;
363 };
364
365 if item.is_none() && push_epoch != self.push_epoch.load(Ordering::Acquire) {
366 #[cfg(any(shuttle, loom))]
367 backoff.backoff();
368 continue;
369 }
370
371 if item.is_some() || push_epoch == pop_epoch {
372 return item;
373 }
374
375 // else do the second collect pass if we come from the slow path
376 #[cfg(any(loom, shuttle))]
377 backoff.backoff();
378 break;
379 }
380 }
381
382 // there was a linearizable time point during the iteration where both collections where truly empty
383 None
384 }
385
386 fn capacity(&self) -> usize {
387 // the capacity of the currently active collection, i.e. the number of elements that can be pushed directly after resize
388 loop {
389 let push_epoch = self.push_epoch.load(Ordering::Acquire);
390 if !self.register_push(push_epoch) {
391 continue;
392 }
393 let cap = self.get_queue(push_epoch).capacity();
394 self.deregister_reader(push_epoch);
395 return cap;
396 }
397 }
398
399 fn len(&self) -> usize {
400 // the total elements in the collections. Note that len can be > capacity.
401 loop {
402 let push_epoch = self.push_epoch.load(Ordering::Acquire);
403 if !self.register_push(push_epoch) {
404 continue;
405 }
406
407 let pop_epoch = self.pop_epoch.load(Ordering::Acquire);
408 let pop_len = if pop_epoch != push_epoch {
409 if !self.register_pop(pop_epoch) {
410 self.deregister_reader(push_epoch);
411 continue;
412 }
413
414 let pop_len = self.get_queue(pop_epoch).len();
415 self.deregister_reader(pop_epoch);
416 pop_len
417 } else {
418 0
419 };
420
421 let len = self.get_queue(push_epoch).len() + pop_len;
422 self.deregister_reader(push_epoch);
423 return len;
424 }
425 }
426
427 fn is_empty(&self) -> bool {
428 // the collection is empty if pop() returns None
429 loop {
430 let push_epoch = self.push_epoch.load(Ordering::Acquire);
431 if !self.register_push(push_epoch) {
432 continue;
433 }
434
435 let pop_epoch = self.pop_epoch.load(Ordering::Acquire);
436 let pop_is_empty = if pop_epoch != push_epoch {
437 if !self.register_pop(pop_epoch) {
438 self.deregister_reader(push_epoch);
439 continue;
440 }
441
442 let pop_is_empty = self.get_queue(pop_epoch).is_empty();
443 self.deregister_reader(pop_epoch);
444 pop_is_empty
445 } else {
446 true
447 };
448
449 let is_empty = self.get_queue(push_epoch).is_empty() && pop_is_empty;
450 self.deregister_reader(push_epoch);
451 return is_empty;
452 }
453 }
454
455 fn is_full(&self) -> bool {
456 // the collection is full if push() fails
457 loop {
458 let push_epoch = self.push_epoch.load(Ordering::Acquire);
459 if !self.register_push(push_epoch) {
460 continue;
461 }
462 let is_full = self.get_queue(push_epoch).is_full();
463 self.deregister_reader(push_epoch);
464
465 return is_full;
466 }
467 }
468
469 fn with_capacity(capacity: usize) -> Self {
470 Resizable::with_capacity(capacity)
471 }
472}
473
474// convenience methods
475
476#[cfg(not(any(loom, shuttle, echeneis)))]
477impl<Q> Resizable<Q> {
478 /// Deconstructs a `Resizable` into its components.
479 ///
480 /// Returns the left and right raw collections currently used by this object.
481 pub fn into_parts(self) -> [Box<Q>; 2] {
482 let left = self.cores[0].swap(null_mut(), Ordering::Acquire);
483 // Safety:
484 // No concurrent owners of this can happen.
485 // This collection was allocated in `new` or in `grow_by` with `Box::into_raw` and was not deallocated since then.
486 // the collection will be dropped after this.
487 let left = unsafe { Box::from_raw(left) };
488
489 let right = self.cores[1].swap(null_mut(), Ordering::Acquire);
490 // Safety:
491 // No concurrent owners of this can happen.
492 // This collection was allocated in `new` or in `grow_by` with `Box::into_raw` and was not deallocated since then.
493 // the collection will be dropped after this.
494 let right = unsafe { Box::from_raw(right) };
495
496 [left, right]
497 }
498
499 /// returns mutable references to both wrapped collections.
500 pub fn parts_mut(&mut self) -> [&mut Q; 2] {
501 // Safety:
502 // We are the only one accessing the wrapped collections.
503 // No need for synchronization.
504 // left and right can only be null during Drop.
505 let left = unsafe { &mut **self.cores[0].get_mut() };
506 // Safety:
507 // We are the only one accessing the wrapped collections.
508 // No need for synchronization.
509 // left and right can only be null during Drop.
510 let right = unsafe { &mut **self.cores[1].get_mut() };
511
512 [left, right]
513 }
514
515 /// Deconstructs a `Resizable` into its currently active component.
516 ///
517 /// Returns the raw collection currently used by this object.
518 pub fn into_current(mut self) -> Box<Q> {
519 let push_epoch = *self.push_epoch.get_mut();
520 let parts = self.into_parts();
521 parts.into_iter().nth(push_epoch % 2).unwrap()
522 }
523
524 /// Returns a mutable reference to the currently active raw collection.
525 pub fn current_mut(&mut self) -> &mut Q {
526 // Safety:
527 // We are the only one accessing the wrapped collections.
528 // No need for synchronization.
529 // left and right can only be null during Drop.
530 unsafe { &mut **self.cores[*self.push_epoch.get_mut() % 2].get_mut() }
531 }
532}
533
534#[cfg(not(any(loom, shuttle, echeneis)))]
535impl<Q> Resizable<Q>
536where
537 Q: BoundedCollection,
538{
539 /// Attempts to pop an item from the collection.
540 ///
541 /// This method cicumvents the logic synchronization of `Resizable::try_pop`.
542 pub fn pop_mut(&mut self) -> Option<Q::Item> {
543 let pop_idx = *self.pop_epoch.get_mut() % 2;
544 let push_idx = *self.push_epoch.get_mut() % 2;
545
546 if pop_idx == push_idx {
547 self.current_mut().try_pop()
548 } else {
549 let parts = self.parts_mut();
550 parts[pop_idx].try_pop().or(parts[push_idx].try_pop())
551 }
552 }
553
554 /// Attmepts to push an item into the collection.
555 ///
556 /// This method cicumvents the synchronization logic of `Resizable::try_push`.
557 pub fn push_mut(&mut self, item: Q::Item) -> Result<(), Q::Item> {
558 self.current_mut().try_push(item)
559 }
560
561 /// Clears the collection.
562 pub fn clear(&mut self) {
563 for core in self.parts_mut() {
564 while core.try_pop().is_some() {}
565 }
566 }
567
568 /// Migrates all remaining stale items in the old queue into the currently active queue, while ensuring enough capacity
569 pub fn migrate(&mut self) {
570 let pop_epoch = *self.pop_epoch.get_mut();
571 let push_epoch = *self.push_epoch.get_mut();
572
573 if pop_epoch == push_epoch {
574 return;
575 }
576
577 let parts = self.parts_mut();
578 let pop_queue = &parts[pop_epoch % 2];
579 let push_queue = &parts[push_epoch % 2];
580
581 let total_items = pop_queue.len() + push_queue.len();
582 let empty_collection = Q::with_capacity(total_items);
583
584 while let Some(item) = pop_queue.try_pop() {
585 _ = empty_collection.try_push(item);
586 }
587
588 while let Some(item) = push_queue.try_pop() {
589 _ = empty_collection.try_push(item);
590 }
591
592 let new_queue = Box::into_raw(Box::new(empty_collection));
593
594 // Safety:
595 // since pop_epoch == push_epoch all concurrent threads access the queue at push_epoch % 2.
596 // pop ensures that no pushes are in flight to the old queue anymore and that it is empty. We can safely drop it.
597 let old_queue = self.cores[pop_epoch % 2].swap(new_queue, Ordering::Relaxed);
598
599 self.push_epoch.fetch_add(1, Ordering::Relaxed);
600 self.pop_epoch.store(push_epoch + 1, Ordering::Relaxed);
601
602 // Safety:
603 // old_queue was consrtucted from a Box::into_raw and is dropped only once, as ensured by epoch guards
604 let _q = unsafe { Box::from_raw(old_queue) };
605 }
606
607 // TODO add drain(..)
608}
609
610#[cfg(not(any(loom, shuttle, echeneis)))]
611impl<Q: BoundedCollection> Extend<Q::Item> for Resizable<Q> {
612 fn extend<I: IntoIterator<Item = Q::Item>>(&mut self, iter: I) {
613 for item in iter {
614 if let Err(item) = self.push_mut(item) {
615 // we ran out of space
616 // ensure that we can resize
617 self.migrate();
618 let cap = self.capacity();
619 // make more space
620 self.resize(cap * 2);
621 _ = self.push_mut(item);
622 }
623 }
624 }
625}
626
627/// An iterator over a [`Resizable`]
628pub struct IntoIter<Q: BoundedCollection> {
629 old: Option<Box<Q>>,
630 new: Option<Box<Q>>,
631}
632
633impl<Q: BoundedCollection> Iterator for IntoIter<Q> {
634 type Item = Q::Item;
635
636 fn next(&mut self) -> Option<Self::Item> {
637 if let Some(old_q) = &mut self.old {
638 if let Some(item) = old_q.try_pop() {
639 return Some(item);
640 }
641 self.old = None;
642 }
643
644 self.new.as_mut().and_then(|q| q.try_pop())
645 }
646}
647
648#[cfg(not(any(loom, shuttle, echeneis)))]
649impl<Q: BoundedCollection> IntoIterator for Resizable<Q> {
650 type IntoIter = IntoIter<Q>;
651 type Item = Q::Item;
652
653 fn into_iter(mut self) -> Self::IntoIter {
654 let push_epoch = *self.push_epoch.get_mut();
655 let [left, right] = self.into_parts();
656
657 #[allow(clippy::manual_is_multiple_of)]
658 let (old, new) = if push_epoch % 2 == 0 {
659 (right, left)
660 } else {
661 (left, right)
662 };
663
664 IntoIter {
665 old: Some(old),
666 new: Some(new),
667 }
668 }
669}