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