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