Skip to main content

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}